DeepakNess - BlogPosts on what I'm building, learning, and thinking about.2026-03-09T00:18:03Zhttps://deepakness.com/https://deepakness.com/img/deepakness.pnghttps://deepakness.com/img/deepakness.pngDeepakNessSelf-hosting FreshRSS on a Raspberry Pi device2026-03-09T00:18:03Zhttps://deepakness.com/blog/freshrss-on-pi/A quick guide to self-hosting FreshRSS, an open-source feed reader, on the Raspberry Pi 4B with 1GB of RAM.<p>I love <a rel="noreferrer" target="_blank" href="https://courses.ischool.berkeley.edu/i290-14/s05/lecture-23/allslides.html">RSS/Atom</a> and am subscribed to at least 75 feeds via the <a rel="noreferrer" target="_blank" href="https://netnewswire.com">NetNewsWire app</a> on my macOS laptop. While the app works great, sometimes, I want to read blogs on my phone too and my current setup didn't allow that. I was thinking to solve this for me for a long time, and finally got the idea and time to build a better reading setup when I learned about <a rel="noreferrer" target="_blank" href="https://github.com/FreshRSS/FreshRSS">FreshRSS</a> and that it can also be self-hosted.</p>
<p>Initially, I was ready to self-host this on a tiny VPS, but then I decided to use the tiny <a rel="noreferrer" target="_blank" href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/">Raspberry Pi 4B with 1GB RAM</a> that I had. Now that the setup is ready and I am already using it, it only consumes <250MB of RAM on the Pi device.</p>
<p><img alt="Raspberry Pi 4B with 1GB RAM" decoding="async" src="https://assets.deepakness.com/blog/freshrss-on-pi/r-pi-4b-1gb.jpg"></p>
<p>In this post, I will note down the whole installation process and my experience at the end:</p>
<h2>1. Installing Docker</h2>
<p>Docker made the process of installing and maintaining FreshRSS easier, and it was also the <a rel="noreferrer" target="_blank" href="https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/README.md">recommended way</a> to install the app.</p>
<p>I, first, connected to my Pi device from my macOS laptop via SSH and then ran the following commands to install and start Docker.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token comment"># Refresh package lists</span>
<span class="token function">sudo</span> <span class="token function">apt</span> update
<span class="token comment"># Install Docker engine + CLI + Compose</span>
<span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> docker.io docker-cli <span class="token function">docker-compose</span>
<span class="token comment"># Start Docker now and on boot</span>
<span class="token function">sudo</span> systemctl <span class="token builtin class-name">enable</span> <span class="token parameter variable">--now</span> <span class="token function">docker</span>
<span class="token comment"># Test iif everything is working</span>
<span class="token function">docker</span> <span class="token parameter variable">--version</span>
<span class="token function">docker</span> compose version
<span class="token function">sudo</span> <span class="token function">docker</span> run hello-world</code></pre>
<p>Earlier, my device was using ~100MB of RAM when it had nothing installed, and now it was using ~180-ish MB of RAM.</p>
<h2>2. Installing FreshRSS on the Pi</h2>
<p>As Docker was ready, I proceeded with installing the FreshRSS app.</p>
<p>First, created a new folder in the root called <code>freshrss</code> and a few subfolders by running the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token comment"># Create a new folder and subfolders</span>
<span class="token function">mkdir</span> <span class="token parameter variable">-p</span> ~/freshrss/data ~/freshrss/extensions
<span class="token comment"># Go into the newly created folder</span>
<span class="token builtin class-name">cd</span> ~/freshrss</code></pre>
<p>Created the <code>compose.yaml</code> file in the <code>freshrss</code> directory with the following content. I used the <code>nano compose.yaml</code> command to create the file, and then pasted the following:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token key atrule">services</span><span class="token punctuation">:</span>
<span class="token key atrule">freshrss</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> freshrss/freshrss<span class="token punctuation">:</span>latest
<span class="token key atrule">container_name</span><span class="token punctuation">:</span> freshrss
<span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped
<span class="token key atrule">ports</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token string">"127.0.0.1:8080:80"</span>
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token key atrule">TZ</span><span class="token punctuation">:</span> Asia/Kolkata
<span class="token key atrule">CRON_MIN</span><span class="token punctuation">:</span> <span class="token string">"13,43"</span>
<span class="token key atrule">volumes</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> ./data<span class="token punctuation">:</span>/var/www/FreshRSS/data
<span class="token punctuation">-</span> ./extensions<span class="token punctuation">:</span>/var/www/FreshRSS/extensions
<span class="token key atrule">healthcheck</span><span class="token punctuation">:</span>
<span class="token key atrule">test</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">"CMD"</span><span class="token punctuation">,</span> <span class="token string">"cli/health.php"</span><span class="token punctuation">]</span>
<span class="token key atrule">interval</span><span class="token punctuation">:</span> 75s
<span class="token key atrule">timeout</span><span class="token punctuation">:</span> 10s
<span class="token key atrule">retries</span><span class="token punctuation">:</span> <span class="token number">3</span>
<span class="token key atrule">start_period</span><span class="token punctuation">:</span> 60s</code></pre>
<p>You might need to modify the TZ (timezone) value, as per your location. And then started the container:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token comment"># Start the docker container</span>
<span class="token function">sudo</span> <span class="token function">docker</span> compose up <span class="token parameter variable">-d</span>
<span class="token comment"># Check status using these commands</span>
<span class="token function">sudo</span> <span class="token function">docker</span> compose <span class="token function">ps</span></code></pre>
<p>If everything works, exit using <code>ctrl + c</code>.</p>
<p>Now, the app is ready and running at the port <code>8080</code>, but you can't access it from your different devices yet. From here, you have 2 options:</p>
<ol>
<li>Use Tailscale on all your devices to access</li>
<li>Expose the app to the internet using Cloudflare Tunnel</li>
</ol>
<blockquote>
<p>From the security and ease-of-setup point of view, the #1 method is the best here. But I also wanted to give access to the feed reader app to my friend, so I went with the #2 option.</p>
</blockquote>
<p>I will explain the Cloudflare Tunnel method here.</p>
<h2>3. Exposing the app to the public internet</h2>
<p>First, I needed to install <code>cloudflared</code> on my Pi, so I ran following commands one by one:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">mkdir</span> <span class="token parameter variable">-p</span> <span class="token parameter variable">--mode</span><span class="token operator">=</span>0755 /usr/share/keyrings
<span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://pkg.cloudflare.com/cloudflare-main.gpg <span class="token operator">|</span> <span class="token function">sudo</span> <span class="token function">tee</span> /usr/share/keyrings/cloudflare-main.gpg <span class="token operator">></span>/dev/null
<span class="token builtin class-name">echo</span> <span class="token string">'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main'</span> <span class="token operator">|</span> <span class="token punctuation">\</span>
<span class="token function">sudo</span> <span class="token function">tee</span> /etc/apt/sources.list.d/cloudflared.list
<span class="token function">sudo</span> <span class="token function">apt</span> update
<span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> cloudflared</code></pre>
<p>I also had to sign up to my Cloudflare account, go to <code>Networking > Tunnels</code> section, and create a Tunnel. Just gave the tunnel a name, and it gave me a command like below that I ran on the Pi:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token comment"># Connect to the Cloudflare dashboard</span>
<span class="token function">sudo</span> cloudflared <span class="token function">service</span> <span class="token function">install</span> a_very_long_string_here
<span class="token comment"># Keep the tunnel running</span>
<span class="token function">sudo</span> systemctl <span class="token builtin class-name">enable</span> <span class="token parameter variable">--now</span> cloudflared</code></pre>
<p>After this I added a destination subdomain, and then added <code>http://localhost:8080</code> as the service URL, and the app was live.</p>
<p>I opened the destination subdomain I set up in the browser, and FreshRSS installation checklist was shown, then it asked me to sign up for the admin account, and I was in. I tried adding an RSS feed for testing, and it was working.</p>
<h2>4. Importing my reading list and final look</h2>
<p>I maintain this <a rel="noreferrer" target="_blank" href="https://deepakness.com/blogroll/">blogroll page</a> on my website, so I added these feeds to the app. And then I also imported a <code>.opml</code> file <a rel="noreferrer" target="_blank" href="https://x.com/karpathy/status/2018043254986703167">shared by Andrej Karpathy</a> earlier, it contained <a rel="noreferrer" target="_blank" href="https://gist.github.com/emschwartz/e6d2bf860ccc367fe37ff953ba6de66b">90+ RSS/Atom feeds</a> of most popular blogs on HackerNews.</p>
<p>I probably don't need all these, but as I spend more time reading, I will slowly keep unsubscribing to the feeds where the content is not relevant to me.</p>
<p>Apart from this, I also did a bit of customizations from the settings, and below is the final look for the reader. The sidebar can be toggled off, so the overall look is cleaner.</p>
<p><img alt="FreshRSS final look for me" decoding="async" src="https://assets.deepakness.com/blog/freshrss-on-pi/freshrss-final-look.png"></p>
<p>Initially, it required getting used to the UI as I have only used NetNewsWire previously and that is simpler to use. But now I like this as well, and have already been using it for a few days.</p>
<h2>How the Raspberry Pi 4B is performing</h2>
<p>The Pi device is performing surprisingly well. Currently, me and friend both are using the self-hosted RSS reader, and it doesn't even lag or slow down, even for a bit. And still only uses less than 250 MB of RAM, sometimes, even less than 200 MB, as you see below:</p>
<p><img alt="RAM usage for Pi 4B" decoding="async" src="https://assets.deepakness.com/blog/freshrss-on-pi/ram-usage-for-pi-4b.png"></p>
<p>I am not on an extremely fast but on a 200 Mbps fiber internet connection (I mean, people already moved to Gbps, right?), and loading is still very fast – faster than many managed RSS reader web-apps I have tried.</p>
<p><img alt="Raspberry Pi connected to my WiFi router" decoding="async" src="https://assets.deepakness.com/blog/freshrss-on-pi/pi-connected-to-router.png"></p>
<p>Also, earlier, I had the Pi device connected to a power adapter via a type-c cable. But then I noticed a USB port on my WiFi router and connected to it, and now it takes the power from the router itself, as you see in the photo above.</p>
<h2>What I like about FreshRSS</h2>
<p>Some things I love about the <a rel="noreferrer" target="_blank" href="https://freshrss.org/index.html">open-source FreshRSS</a> app are:</p>
<ol>
<li>It fetches feed updates on the server-side in the background, so I don't have to wait for 2 minutes for all 90 feeds to get refreshed. When I open the public URL in the browser, it's ready for me to read as new updates are already fetched.</li>
<li>When you mark a post as favorite, it's permanently saved in the SQLite database it uses. Even if the original version is deleted, you still have the locally saved version.</li>
</ol>
<p>FreshRSS has tons of options that I am still exploring and learning about, and will keep this page updated as I discover more noteworthy things.</p>
How I Sync Mastodon Posts with My 11ty Blog2026-02-06T16:52:48Zhttps://deepakness.com/blog/mastodon-sync/A walkthrough of a Node.js script that maps Mastodon statuses to blog slugs and updates frontmatter.<p>I don't have a comment system on my blog, so I started adding this <a rel="noreferrer" target="_blank" href="https://deepakness.com/raw/comment-via-email/">comment via email</a> almost a year ago, and received a lot of heartwarming replies through that option. But not everyone wants to email you, so I recently also started adding this "Comment on Mastodon" feature below my blog posts (along with <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/adding-webmentions/">webmentions</a>).</p>
<p><img alt="Comment on Mastodon link in the footer" decoding="async" src="https://assets.deepakness.com/blog/mastodon-sync/comment-on-mastodon-link.png"></p>
<p>Typically, here's the process that I would follow:</p>
<ol>
<li>Publish a blog post</li>
<li>Share it on Mastodon, and</li>
<li>Add Mastodon post link in Markdown frontmatter</li>
</ol>
<p>When you add the <code>mastodon</code> YAML frontmatter in a post, the "Comment on Mastodon" option automatically starts showing below the post. Here's what the frontmatter looks like:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token punctuation">---</span>
<span class="token key atrule">date</span><span class="token punctuation">:</span> <span class="token datetime number">2025-01-01</span>
<span class="token key atrule">title</span><span class="token punctuation">:</span> <span class="token string">"title"</span>
<span class="token key atrule">description</span><span class="token punctuation">:</span> <span class="token string">"description"</span>
<span class="token key atrule">tags</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> tag<span class="token punctuation">-</span>one
<span class="token punctuation">---</span></code></pre>
<p>But... it's a tedious process to manually do it each time, isn't it?</p>
<p>I wanted something semi-automated, and I have finally put together a workflow that works for me.</p>
<ol>
<li>I publish a blog post.</li>
<li>It's automatically published to Mastodon, Threads, and Bluesky via n8n by using <a rel="noreferrer" target="_blank" href="https://go.deepakness.com/typefully">Typefully</a> API (<a rel="noreferrer" target="_blank" href="https://deepakness.com/raw/typefully-api-n8n/">see how it's done</a>).</li>
<li>Run a script to auto-fetch, find your Mastodon posts with your blog URLs and update the frontmatter.</li>
</ol>
<blockquote>
<p>I already use <a rel="noreferrer" target="_blank" href="https://go.deepakness.com/typefully">Typefully</a> to schedule my posts on social media, so it's not an additional purchase for me. But you can automate publishing to Mastodon directly via their API.</p>
</blockquote>
<p>I am going to paste the entire script that I use for this, you can save it inside, say, <code>/scripts/mastodon.js</code> file.</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span> fs <span class="token keyword">from</span> <span class="token string">'fs'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> path <span class="token keyword">from</span> <span class="token string">'path'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> fileURLToPath <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'url'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> __filename <span class="token operator">=</span> <span class="token function">fileURLToPath</span><span class="token punctuation">(</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> __dirname <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">dirname</span><span class="token punctuation">(</span>__filename<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">ROOT_DIR</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">resolve</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> <span class="token string">'..'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">CONTENT_DIR</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">ROOT_DIR</span><span class="token punctuation">,</span> <span class="token string">'content'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">MASTODON_INSTANCE</span> <span class="token operator">=</span> <span class="token string">'https://mastodon.social'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">MASTODON_USERNAME</span> <span class="token operator">=</span> <span class="token string">'deepakness'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">BLOG_DOMAIN</span> <span class="token operator">=</span> <span class="token string">'deepakness.com'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">DRY_RUN</span> <span class="token operator">=</span> process<span class="token punctuation">.</span>argv<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">'--dry-run'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">FORCE</span> <span class="token operator">=</span> process<span class="token punctuation">.</span>argv<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">'--force'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Fetch account ID from username</span>
<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">fetchAccountId</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">MASTODON_INSTANCE</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/api/v1/accounts/lookup?acct=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">MASTODON_USERNAME</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Failed to fetch account: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>response<span class="token punctuation">.</span>status<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> account <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> account<span class="token punctuation">.</span>id<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Fetch all statuses with pagination</span>
<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">fetchAllStatuses</span><span class="token punctuation">(</span><span class="token parameter">accountId</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> statuses <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> maxId <span class="token operator">=</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> page <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> url <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">MASTODON_INSTANCE</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/api/v1/accounts/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>accountId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/statuses</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
url<span class="token punctuation">.</span>searchParams<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'limit'</span><span class="token punctuation">,</span> <span class="token string">'40'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
url<span class="token punctuation">.</span>searchParams<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'exclude_reblogs'</span><span class="token punctuation">,</span> <span class="token string">'true'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>maxId<span class="token punctuation">)</span> <span class="token punctuation">{</span>
url<span class="token punctuation">.</span>searchParams<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span><span class="token string">'max_id'</span><span class="token punctuation">,</span> maxId<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Fetching page </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>page<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">...</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Failed to fetch statuses: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>response<span class="token punctuation">.</span>status<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> batch <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>batch<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token keyword">break</span><span class="token punctuation">;</span>
statuses<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token operator">...</span>batch<span class="token punctuation">)</span><span class="token punctuation">;</span>
maxId <span class="token operator">=</span> batch<span class="token punctuation">[</span>batch<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>id<span class="token punctuation">;</span>
page<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token comment">// Small delay to be nice to the API</span>
<span class="token keyword">await</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token parameter">resolve</span> <span class="token operator">=></span> <span class="token function">setTimeout</span><span class="token punctuation">(</span>resolve<span class="token punctuation">,</span> <span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> statuses<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Extract links from HTML content using regex</span>
<span class="token keyword">function</span> <span class="token function">extractLinksFromHtml</span><span class="token punctuation">(</span><span class="token parameter">html</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> links <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> regex <span class="token operator">=</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">href="([^"]+)"</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">;</span>
<span class="token keyword">let</span> match<span class="token punctuation">;</span>
<span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>match <span class="token operator">=</span> regex<span class="token punctuation">.</span><span class="token function">exec</span><span class="token punctuation">(</span>html<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">!==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
links<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>match<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> links<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Parse deepakness.com URL and extract type (blog/raw) and slug</span>
<span class="token keyword">function</span> <span class="token function">parseDeepakNessUrl</span><span class="token punctuation">(</span><span class="token parameter">url</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> parsed <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">URL</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>parsed<span class="token punctuation">.</span>hostname <span class="token operator">!==</span> <span class="token constant">BLOG_DOMAIN</span> <span class="token operator">&&</span> parsed<span class="token punctuation">.</span>hostname <span class="token operator">!==</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">www.</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">BLOG_DOMAIN</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Match /blog/slug or /raw/slug</span>
<span class="token keyword">const</span> match <span class="token operator">=</span> parsed<span class="token punctuation">.</span>pathname<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^\/(blog|raw)\/([^/]+)\/?$</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>match<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token literal-property property">type</span><span class="token operator">:</span> match<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token literal-property property">slug</span><span class="token operator">:</span> match<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token comment">// Find markdown file for a given type and slug</span>
<span class="token keyword">function</span> <span class="token function">findMarkdownFile</span><span class="token punctuation">(</span><span class="token parameter">type<span class="token punctuation">,</span> slug</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> possiblePaths <span class="token operator">=</span> <span class="token punctuation">[</span>
path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">CONTENT_DIR</span><span class="token punctuation">,</span> type<span class="token punctuation">,</span> slug<span class="token punctuation">,</span> <span class="token string">'index.md'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">CONTENT_DIR</span><span class="token punctuation">,</span> type<span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.md</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> filePath <span class="token keyword">of</span> possiblePaths<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>filePath<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> filePath<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Check if frontmatter already has mastodon field</span>
<span class="token keyword">function</span> <span class="token function">hasMastodonField</span><span class="token punctuation">(</span><span class="token parameter">content</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> frontmatterMatch <span class="token operator">=</span> content<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^---\n([\s\S]*?)\n---</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>frontmatterMatch<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^mastodon:</span><span class="token regex-delimiter">/</span><span class="token regex-flags">m</span></span><span class="token punctuation">.</span><span class="token function">test</span><span class="token punctuation">(</span>frontmatterMatch<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Insert mastodon field into frontmatter</span>
<span class="token keyword">function</span> <span class="token function">insertMastodonField</span><span class="token punctuation">(</span><span class="token parameter">content<span class="token punctuation">,</span> mastodonUrl</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// Insert after the opening ---</span>
<span class="token keyword">return</span> content<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^---\n</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">---\nmastodon: "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>mastodonUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Scan content directories and find files (optionally only those missing mastodon field)</span>
<span class="token keyword">function</span> <span class="token function">findContentFiles</span><span class="token punctuation">(</span><span class="token parameter">onlyMissing <span class="token operator">=</span> <span class="token boolean">true</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> files <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> contentTypes <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">'blog'</span><span class="token punctuation">,</span> <span class="token string">'raw'</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> type <span class="token keyword">of</span> contentTypes<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> typeDir <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">CONTENT_DIR</span><span class="token punctuation">,</span> type<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>typeDir<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">continue</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> entries <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readdirSync</span><span class="token punctuation">(</span>typeDir<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">withFileTypes</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> entry <span class="token keyword">of</span> entries<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> filePath<span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>entry<span class="token punctuation">.</span><span class="token function">isDirectory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
filePath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>typeDir<span class="token punctuation">,</span> entry<span class="token punctuation">.</span>name<span class="token punctuation">,</span> <span class="token string">'index.md'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>entry<span class="token punctuation">.</span>name<span class="token punctuation">.</span><span class="token function">endsWith</span><span class="token punctuation">(</span><span class="token string">'.md'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
filePath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>typeDir<span class="token punctuation">,</span> entry<span class="token punctuation">.</span>name<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
<span class="token keyword">continue</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>filePath<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">continue</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> content <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span>filePath<span class="token punctuation">,</span> <span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> hasMastodon <span class="token operator">=</span> <span class="token function">hasMastodonField</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>onlyMissing <span class="token operator">||</span> <span class="token operator">!</span>hasMastodon<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> slug <span class="token operator">=</span> entry<span class="token punctuation">.</span><span class="token function">isDirectory</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">?</span> entry<span class="token punctuation">.</span>name <span class="token operator">:</span> entry<span class="token punctuation">.</span>name<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">'.md'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
files<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span><span class="token punctuation">{</span> type<span class="token punctuation">,</span> slug<span class="token punctuation">,</span> filePath<span class="token punctuation">,</span> hasMastodon <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> files<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Main function</span>
<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'\n🐘 Syncing Mastodon links to markdown files...\n'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token constant">DRY_RUN</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'🔍 DRY RUN MODE - No files will be modified\n'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token constant">FORCE</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'🔄 FORCE MODE - Will overwrite existing mastodon fields\n'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// First, check which files need updating</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Scanning content files...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> filesToUpdate <span class="token operator">=</span> <span class="token function">findContentFiles</span><span class="token punctuation">(</span><span class="token operator">!</span><span class="token constant">FORCE</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> label <span class="token operator">=</span> <span class="token constant">FORCE</span> <span class="token operator">?</span> <span class="token string">'files to check'</span> <span class="token operator">:</span> <span class="token string">'files missing mastodon field'</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Found </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>filesToUpdate<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>label<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>filesToUpdate<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'✨ All files already have mastodon field. Nothing to do!\n'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Create a set for quick lookup</span>
<span class="token keyword">const</span> updateSet <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Set</span><span class="token punctuation">(</span>filesToUpdate<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">f</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>f<span class="token punctuation">.</span>type<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>f<span class="token punctuation">.</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Fetch account ID</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Looking up account @</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span><span class="token constant">MASTODON_USERNAME</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">...</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> accountId <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchAccountId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Found account ID: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>accountId<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Fetch all statuses</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Fetching all statuses...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> statuses <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetchAllStatuses</span><span class="token punctuation">(</span>accountId<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Found </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>statuses<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> statuses\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Build map of blog URLs to mastodon URLs (earliest status per slug)</span>
<span class="token keyword">const</span> matchMap <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Map</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// key: "type/slug", value: match object</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> status <span class="token keyword">of</span> statuses<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>status<span class="token punctuation">.</span>content<span class="token punctuation">)</span> <span class="token keyword">continue</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> links <span class="token operator">=</span> <span class="token function">extractLinksFromHtml</span><span class="token punctuation">(</span>status<span class="token punctuation">.</span>content<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> link <span class="token keyword">of</span> links<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> parsed <span class="token operator">=</span> <span class="token function">parseDeepakNessUrl</span><span class="token punctuation">(</span>link<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>parsed<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> key <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>parsed<span class="token punctuation">.</span>type<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>parsed<span class="token punctuation">.</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token comment">// Always overwrite to keep the earliest (last in reverse-chronological order)</span>
matchMap<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">type</span><span class="token operator">:</span> parsed<span class="token punctuation">.</span>type<span class="token punctuation">,</span>
<span class="token literal-property property">slug</span><span class="token operator">:</span> parsed<span class="token punctuation">.</span>slug<span class="token punctuation">,</span>
<span class="token literal-property property">mastodonUrl</span><span class="token operator">:</span> status<span class="token punctuation">.</span>url<span class="token punctuation">,</span>
<span class="token literal-property property">blogUrl</span><span class="token operator">:</span> link<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token comment">// Filter to only matches for files that need updating</span>
<span class="token keyword">const</span> matches <span class="token operator">=</span> Array<span class="token punctuation">.</span><span class="token function">from</span><span class="token punctuation">(</span>matchMap<span class="token punctuation">.</span><span class="token function">values</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">m</span> <span class="token operator">=></span> updateSet<span class="token punctuation">.</span><span class="token function">has</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>m<span class="token punctuation">.</span>type<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>m<span class="token punctuation">.</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Found </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>matches<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> Mastodon posts for files needing update\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Process matches</span>
<span class="token keyword">let</span> updated <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> skipped <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> match <span class="token keyword">of</span> matches<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> filePath <span class="token operator">=</span> <span class="token function">findMarkdownFile</span><span class="token punctuation">(</span>match<span class="token punctuation">.</span>type<span class="token punctuation">,</span> match<span class="token punctuation">.</span>slug<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>filePath<span class="token punctuation">)</span> <span class="token keyword">continue</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> relativePath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">relative</span><span class="token punctuation">(</span><span class="token constant">ROOT_DIR</span><span class="token punctuation">,</span> filePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> content <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span>filePath<span class="token punctuation">,</span> <span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Check if file already has this exact mastodon URL (skip if same)</span>
<span class="token keyword">const</span> existingMatch <span class="token operator">=</span> content<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^mastodon:\s*"([^"]+)"</span><span class="token regex-delimiter">/</span><span class="token regex-flags">m</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>existingMatch <span class="token operator">&&</span> existingMatch<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">===</span> match<span class="token punctuation">.</span>mastodonUrl<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">⏭️ Already correct: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>relativePath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
skipped<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token keyword">continue</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Remove existing mastodon field if present (for --force)</span>
<span class="token keyword">let</span> newContent <span class="token operator">=</span> content<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^mastodon:\s*"[^"]+"\n</span><span class="token regex-delimiter">/</span><span class="token regex-flags">m</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
newContent <span class="token operator">=</span> <span class="token function">insertMastodonField</span><span class="token punctuation">(</span>newContent<span class="token punctuation">,</span> match<span class="token punctuation">.</span>mastodonUrl<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token constant">DRY_RUN</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">📝 Would update: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>relativePath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> Mastodon URL: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>match<span class="token punctuation">.</span>mastodonUrl<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
fs<span class="token punctuation">.</span><span class="token function">writeFileSync</span><span class="token punctuation">(</span>filePath<span class="token punctuation">,</span> newContent<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">✅ Updated: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>relativePath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
updated<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Files that had no matching Mastodon post</span>
<span class="token keyword">const</span> notFound <span class="token operator">=</span> filesToUpdate<span class="token punctuation">.</span>length <span class="token operator">-</span> updated <span class="token operator">-</span> skipped<span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">\n✨ Done!</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> Updated: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>updated<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>skipped <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> Already correct: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>skipped<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> No Mastodon post found: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>notFound<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span><span class="token parameter">error</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'❌ Error:'</span><span class="token punctuation">,</span> error<span class="token punctuation">.</span>message<span class="token punctuation">)</span><span class="token punctuation">;</span>
process<span class="token punctuation">.</span><span class="token function">exit</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The above Node.js script uses public Mastodon API endpoints that do not require authentication, so you don't even need an API key. It uses <code>fs</code>, <code>path</code>, <code>url</code>, and <code>fetch</code> libraries which are Node.js built-ins.</p>
<p>When I run the script in my terminal by running either <code>npm run mastodon</code> or <code>node scripts/mastodon.js</code> command in my terminal:</p>
<ol>
<li>Scans my local markdown files in <code>content/blog/</code> and <code>content/raw/</code> folders, identifies which ones are missing a <code>mastodon</code>: field in their YAML frontmatter.</li>
<li>Fetches all posts from the <code>@deepakness</code> Mastodon account via the public API by paginating through all posts I have.</li>
<li>Matches posts to blog entries by scanning each Mastodon post's HTML content for links to <code>deepakness.com/blog/[slug]</code> or <code>deepakness.com/raw/[slug]</code>, and then maps those back to the related local markdown files.</li>
<li>Updates the frontmatter of each matched markdown file by inserting a <code>mastodon: "https://mastodon.social/..."</code> field.</li>
</ol>
<p>The script also has two optional flags:</p>
<ul>
<li><code>--dry-run</code> shows what would change without writing anything</li>
<li><code>--force</code> overwrites existing <code>mastodon:</code> fields instead of skipping files that already have one</li>
</ul>
<p>Also, when multiple Mastodon posts link to the same blog post, it keeps the earliest one (since posts are fetched in reverse-chronological order, the last match per slug is fetched).</p>
<blockquote>
<p>Most probably, you won't be able to directly use this script as it's specific to how my 11ty blog is structured. But you can give the script to any LLM, provide info on how your static blog is structured, and ask to provide the modified version of the script.</p>
</blockquote>
<p>I manually run the script every few days, so you won't see the "Comment on Mastodon" link on new posts right away – but it shows up soon enough.</p>
Dynamic Open Graph Images for My 11ty Blog2026-02-06T09:18:45Zhttps://deepakness.com/blog/custom-og-images/A quick guide to how I dynamically generate custom Open Graph images for each post on my 11ty blog, using satori.<p>Apart from <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/adding/webmentions/">adding webmentions</a> and <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/better-images/">better handling images</a>, I also recently finished setting up dynamic Open Graph image generation on my 11ty blog, and it is now working for blog posts, raw notes, and standalone pages.</p>
<p>Earlier I was using just one default social image for all pages and posts I had. It worked, but all shared links looked repetitive. So I created a local generation script and connected it into my existing workflow.</p>
<p>In this post, I will show the exact setup I am using.</p>
<h2>1. How OG image is selected on my website</h2>
<p>I use computed data to decide what image goes into <code>og:image</code> and <code>twitter:image</code> and have this file <code>_data/eleventyComputed.js</code> with the following content:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">{</span>
<span class="token function-variable function">ogImage</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">data</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token comment">// If the post has a custom image in frontmatter, use that</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>image<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> data<span class="token punctuation">.</span>image<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// For blog posts and raw notes, use the co-located og.jpeg</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>page <span class="token operator">&&</span> data<span class="token punctuation">.</span>page<span class="token punctuation">.</span>url<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">.</span>page<span class="token punctuation">.</span>url<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'/blog/'</span><span class="token punctuation">)</span> <span class="token operator">||</span> data<span class="token punctuation">.</span>page<span class="token punctuation">.</span>url<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span><span class="token string">'/raw/'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>data<span class="token punctuation">.</span>page<span class="token punctuation">.</span>url<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">og.jpeg</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token comment">// Fall back to default metadata image</span>
<span class="token keyword">return</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>And in my base template, <code>_includes/layouts/base.njk</code>, both Open Graph and Twitter tags read this value:</p>
<p><img alt="OG value from base.njk" decoding="async" src="https://assets.deepakness.com/blog/custom-og-images/og-value-base-njk.png"></p>
<p>So the priority is:</p>
<ol>
<li><code>image</code> from frontmatter (manual custom image)</li>
<li>Generated co-located <code>og.jpeg</code> for <code>/blog/*</code> and <code>/raw/*</code></li>
<li>Site default fallback image from metadata</li>
</ol>
<p><img alt="Final OG image URL value" decoding="async" src="https://assets.deepakness.com/blog/custom-og-images/final-url-value.png"></p>
<p>And then the final Open Graph image URL looks something like how it looks in the above screenshot.</p>
<h2>2. The generator script I use</h2>
<p>I use <code>sharp</code> image library with a Node.js script at <code>scripts/generate-og-images.js</code>. And the script does the following:</p>
<ol>
<li>Scans <code>content/blog</code> and <code>content/raw</code></li>
<li>Scans top-level pages in <code>content/</code></li>
<li>Handles homepage as a special case</li>
<li>Skips items that already have <code>og.png</code>, <code>og.jpeg</code>, or <code>og.jpg</code></li>
<li>Skips items that already have <code>image</code> in frontmatter</li>
<li>Uses MD5 cache in <code>.og-cache.json</code> to skip unchanged items</li>
<li>Outputs <code>1200x630</code> JPEG images</li>
</ol>
<p>It uses the <a rel="noreferrer" target="_blank" href="https://github.com/vercel/satori">Vercel's satori library</a> for layout, <code>@resvg/resvg-js</code> for rendering SVG to PNG, and <code>sharp</code> to produce final JPEG. Here are some useful snippets from the <code>generate-og-images.js</code> script.</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> <span class="token constant">CACHE_FILE</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>rootDir<span class="token punctuation">,</span> <span class="token string">'.og-cache.json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">BLOG_DIR</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>rootDir<span class="token punctuation">,</span> <span class="token string">'content'</span><span class="token punctuation">,</span> <span class="token string">'blog'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">RAW_DIR</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>rootDir<span class="token punctuation">,</span> <span class="token string">'content'</span><span class="token punctuation">,</span> <span class="token string">'raw'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">PAGES_OG_DIR</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>rootDir<span class="token punctuation">,</span> <span class="token string">'public'</span><span class="token punctuation">,</span> <span class="token string">'img'</span><span class="token punctuation">,</span> <span class="token string">'og'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>item<span class="token punctuation">.</span>hasExistingOg <span class="token operator">||</span> item<span class="token punctuation">.</span>hasCustomImage<span class="token punctuation">)</span> <span class="token punctuation">{</span>
skipped<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token keyword">continue</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>cache<span class="token punctuation">[</span>cacheKey<span class="token punctuation">]</span> <span class="token operator">===</span> item<span class="token punctuation">.</span>contentHash <span class="token operator">&&</span> fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>outputPath<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
cached<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token keyword">continue</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">const</span> outputPath <span class="token operator">=</span> item<span class="token punctuation">.</span>collection <span class="token operator">===</span> <span class="token string">'pages'</span>
<span class="token operator">?</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">PAGES_OG_DIR</span><span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>item<span class="token punctuation">.</span>slug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.jpeg</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span>
<span class="token operator">:</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span>folderPath<span class="token punctuation">,</span> <span class="token string">'og.jpeg'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>It's a huge 600 lines of script, so I didn't paste it all here. But you can <a rel="noreferrer" target="_blank" href="https://gist.github.com/deepakness/eab8d275b51418f2c093151dff6d16b4">click this link to view</a> the entire script.</p>
<h2>3. Card design setup</h2>
<p>I kept the design clean and consistent:</p>
<ul>
<li>White background with a subtle grid pattern</li>
<li>Blue accent strip at the top</li>
<li>Large post title and optional description</li>
<li>Footer with profile photo, name, and website label</li>
</ul>
<p>The fonts are loaded from local files, the same ones that I am using on my website, so final images match the aesthetics of the website. By the way, here's the OG image for this post, the one you're currently reading:</p>
<p><img alt="OG image for this post" decoding="async" src="https://assets.deepakness.com/blog/custom-og-images/og-for-this-post.jpeg"></p>
<h2>4. Commands and workflow</h2>
<p>I just run the following command to generate images:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span> run og</code></pre>
<p>In <code>package.json</code>, my local start flow already includes OG generation:</p>
<pre class="language-json" tabindex="0"><code class="language-json"><span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"npm run optimize-images && npm run og && npx @11ty/eleventy --serve --quiet"</span></code></pre>
<p>So in day-to-day writing, I can quickly regenerate social cards before publishing, and also <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/better-images/">optimize images on the go</a>.</p>
<p><img alt="Terminal output after running npm run og script" decoding="async" src="https://assets.deepakness.com/blog/custom-og-images/og-script-terminal-output.png"></p>
<p>And above is the terminal output when I run the script.</p>
<h2>5. Useful edge case: manual override</h2>
<p>If I set an <code>image</code> in frontmatter, generator skips that post on purpose. That lets me keep dynamic generation as default, and still use a custom OG design for specific posts.</p>
<p>Example:</p>
<pre class="language-yaml" tabindex="0"><code class="language-yaml"><span class="token key atrule">image</span><span class="token punctuation">:</span> <span class="token string">"/blog/vivaldi-browser/og.jpg"</span></code></pre>
<p>That is the full setup I am using right now.</p>
<p>It keeps social cards consistent without manual design work on every post, and cache makes regeneration fast when nothing changes. I will keep improving the design over time, but this setup already works well for my workflow.</p>
Adding Webmentions to My 11ty Blog2026-02-03T12:10:53Zhttps://deepakness.com/blog/adding-webmentions/A quick guide to sending and receiving webmentions to my 11ty blog.<p>I was thinking about this for a long but I finally added webmentions to my blog. I had zero knowledge about this from earlier, so I had to do a lot of reading about the <a rel="noreferrer" target="_blank" href="https://www.w3.org/TR/webmention/">webmention protocol</a> to completely understand it. But I did... and it's live on my website. You can <a rel="noreferrer" target="_blank" href="https://deepakness.com/raw/rss-feeds-are-cool-again/">check out this post</a> which has received some webmentions via Mastodon by the time of writing this post.</p>
<p>When you talk about webmentions, receiving and sending them are two different things. And it becomes little tricky to implement this on a static website like mine, which is built using <a rel="noreferrer" target="_blank" href="https://www.11ty.dev">11ty static site generator</a>.</p>
<p>In this post, I will explain my process of how I implemented both the receiving and sending of webmentions. Let's get to it.</p>
<h2>1. Receive incoming webmentions</h2>
<p>When other people link to your website from their blogs or interact with your blog posts on Mastodon or <a rel="noreferrer" target="_blank" href="https://en.wikipedia.org/wiki/Fediverse">Fediverse</a>, they send you webmentions that you need to receive to show on your website.</p>
<h3>Using webmention.io service</h3>
<p>To receive, I used <a rel="noreferrer" target="_blank" href="https://webmention.io">webmention.io</a>, created by <a rel="noreferrer" target="_blank" href="https://www.linkedin.com/in/aaronparecki/">Aaron Parecki</a>, that automatically scans receiving webmentions and lets you access them via an API. When you sign up for it, it gives you a username, and you need to add below <code><link></code> tags to your HTML. It looks like this on my blog:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>webmention<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://webmention.io/deepakness.com/webmention<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pingback<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://webmention.io/deepakness.com/xmlrpc<span class="token punctuation">"</span></span><span class="token punctuation">></span></span></code></pre>
<p>And after you add this, the webmention.io service starts collecting webmentions on your behalf. You can pull these and show on your website via their API.</p>
<p>From here, you need to get the webmention.io API key token that we will need in the next step.</p>
<h3>Showing webmentions on my blog</h3>
<p>Each website is built differently so here I will only explain how I built the setup, and provide you with actual code snippets that I am using.</p>
<p>At first, I created a <code>webmentions.js</code> file in my <code>_data</code> folder with the following content:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span> <span class="token string">"dotenv/config"</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> EleventyFetch <span class="token keyword">from</span> <span class="token string">"@11ty/eleventy-fetch"</span><span class="token punctuation">;</span>
<span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> domain <span class="token operator">=</span> <span class="token string">"deepakness.com"</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> token <span class="token operator">=</span> process<span class="token punctuation">.</span>env<span class="token punctuation">.</span><span class="token constant">WEBMENTION_IO_TOKEN</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>token<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">"WEBMENTION_IO_TOKEN is missing – returning empty webmentions."</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token punctuation">{</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> url <span class="token operator">=</span>
<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">https://webmention.io/api/mentions.jf2?domain=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>domain<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&token=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>token<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">&per-page=1000</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token comment">// Always fetch fresh on each build</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">EleventyFetch</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">duration</span><span class="token operator">:</span> <span class="token string">"6h"</span><span class="token punctuation">,</span>
<span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">"json"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> data<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>I added the API key in the environment variable <code>WEBMENTION_IO_TOKEN</code> on Netlify, as it will be used during the build time.</p>
<p>Then I created a <code>comments.njk</code> file (here are <a rel="noreferrer" target="_blank" href="https://gist.github.com/deepakness/782a1ff050ed52084eb5461bf8a7618a">entire contents of the file</a>, including CSS) in the <code>_includes</code> folder, and used it inside my post template like below:</p>
<p><img alt="Adding comments to post template" decoding="async" src="https://assets.deepakness.com/blog/adding-webmentions/adding-comments-to-post-template.png"></p>
<p>After this, I also used a service called <a rel="noreferrer" target="_blank" href="https://brid.gy">Bridgy</a> that connects some services that do not explicitly send webmentions, still grabs the webmentions from those platforms, and sends them to webmention.io. I added Bluesky to Bridgy and it will now grab comments and reactions from Bluesky as well, even though it doesn't natively support it.</p>
<p>And it was live. I could see some webmentions collected in my webmention.io dashboard and then those pages on my website were also showing them.</p>
<p><img alt="Showing webmentions below posts" decoding="async" src="https://assets.deepakness.com/blog/adding-webmentions/webmentions-below-posts.png"></p>
<p>Above is an example of my current design for showing webmentions below <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog">posts</a> and <a rel="noreferrer" target="_blank" href="https://deepakness.com/raw">notes</a>. I also created this <a rel="noreferrer" target="_blank" href="https://deepakness.com/webmention/">/webmention page</a> that educates people on what this is and how this works on my blog.</p>
<blockquote>
<p>The webmention sections on my blog only updates when I publish something new and the site gets re-built on Netlify.</p>
</blockquote>
<h2>2. Send outgoing webmention notifications</h2>
<p>I am using this amazing tool <a rel="noreferrer" target="_blank" href="https://webmention.app">webmention.app</a>, created by <a rel="noreferrer" target="_blank" href="https://remysharp.com/2019/06/18/send-outgoing-webmentions">Remy Sharp</a>, that automates your outgoing webmentions.</p>
<p>There are multiple ways to use the service, <a rel="noreferrer" target="_blank" href="https://webmention.app/docs">as explained in the docs</a>. I am integrating the service with Netlify by using the "deploy notification" feature.</p>
<ol>
<li>Open your project in Netlify</li>
<li>Go to Project configuration</li>
<li>Scroll down, click on Notifications</li>
<li>Scroll to Deploy notifications under Emails and webhooks section</li>
<li>Click on Add notification, select HTTP POST request option</li>
<li>In Event to listen for: select Deploy succeeded</li>
<li>Enter below URL in URL to notify field</li>
</ol>
<pre class="language-plaintext" tabindex="0"><code class="language-plaintext">https://webmention.app/check?token=[your-token]&limit=1&url=[your-feed-url]</code></pre>
<p><img alt="Netlify deploy notification webhook" decoding="async" src="https://assets.deepakness.com/blog/adding-webmentions/netlify-deploy-notification.png"></p>
<p>Make sure to replace <code>[your-token]</code> with the Token that you get from webmention.app, and <code>[your-feed-url]</code> with the RSS/Atom feed of your website. By the way, there are ways to work without a feed URL as well, you need to <a rel="noreferrer" target="_blank" href="https://webmention.app/docs">go through the docs</a> carefully.</p>
<p>Here are some useful resources that I found helpful during my setup:</p>
<ul>
<li><a rel="noreferrer" target="_blank" href="https://mxb.dev/blog/using-webmentions-on-static-sites/">Using Webmentions in Eleventy</a>, by Max Böck</li>
<li><a rel="noreferrer" target="_blank" href="https://rowanmanning.com/posts/webmentions-for-your-static-site/">Webmentions for your Static Site</a> by Rowan Manning</li>
<li><a rel="noreferrer" target="_blank" href="https://localghost.dev/blog/sending-webmentions-from-a-static-site/">Sending webmentions from a static site</a> by Sophie</li>
<li><a rel="noreferrer" target="_blank" href="https://paul.kinlan.me/using-web-mentions-in-a-static-sitehugo/">Using Web Mentions in a static site (Hugo)</a> by Paul Kinlan</li>
</ul>
<p>I am new to this thing, and I am sure that the setup will evolve to become even better in the future. I will keep this page updated.</p>
Starting a Newsletter using Listmonk2026-02-01T16:57:53Zhttps://deepakness.com/blog/listmonk-setup/I am finally starting a newsletter using self-hosted Listmonk app and Amazon SES.<p>I have been thinking about starting and maintaining a email list for a long time, and today I finally made it live. It wasn't a straightforward process but wasn't extremely complicated either, I had to Google a lot and was finally able to set everything up as I wanted.</p>
<p>For the setup, I used <a rel="noreferrer" target="_blank" href="https://listmonk.app">Listmonk</a> (self-hosted) for managing the list and <a rel="noreferrer" target="_blank" href="https://aws.amazon.com/ses/">Amazon SES</a> for sending the emails. I will try my best to explain the entire process.</p>
<p>But before I start, below is the form if you'd like to subscribe to my list. I have kept it simple to match the aesthetics of my website, and currently only showing the form in the footer.</p>
<p>Now, let's get to it.</p>
<h2>What is Listmonk?</h2>
<p>Listmonk is a self hosted email newsletter and mailing list tool. It lets you collect subscribers, manage lists, design emails, and send campaigns using your own email provider like Amazon SES, Mailgun, Sendgrid, and Postmark.</p>
<p>You control your data, your server, and how emails are sent, without paying per subscriber. It's great for newsletters, product updates, and simple email automation.</p>
<h2>What is Amazon Simple Email Service (SES)?</h2>
<p>Amazon SES (Simple Email Service) is a cloud email sending service from AWS. It is used to send emails at scale for things like newsletters, app notifications, and transactional emails.</p>
<p>You connect your app or tool like Listmonk to SES, and SES handles delivery, spam checks, and inbox reliability while charging based on how many emails you send.</p>
<h2>Self-hosting Listmonk</h2>
<p>I hosted Listmonk on a Hetzner VPS (CX23, costs $3.5/mo) via <a rel="noreferrer" target="_blank" href="https://dokploy.com">Dokploy</a> and it took less than 5 minutes. Here are the steps:</p>
<ol>
<li>Create <a rel="noreferrer" target="_blank" href="https://go.deepakness.com/hetzner">an account on Hetzner</a>, get verified, and create a new server. Even the cheapest server CX23 would be more than enough.</li>
<li>SSH into the server, and install Dokploy by <a rel="noreferrer" target="_blank" href="https://docs.dokploy.com/docs/core/installation">following the instructions</a> on their official website.</li>
<li>Open Dokploy, create a new project and create a new service by selecting the already available Listmonk option. Add a custom domain and then deploy the service.</li>
</ol>
<blockquote>
<p>If you see an outdated version of Listmonk in the Docker Compose file, change it to the latest version. In my case, it was v5.0.0 in the Docker Compose file, so I changed it to v6.0.0 which is the latest.</p>
</blockquote>
<p>After the deployment is successful, visit the domain you added and register a new Listmonk account, and you should now be inside the app.</p>
<h2>Getting the Amazon SES details</h2>
<p>I didn't even have a AWS account so I was thinking it's going to be a complicated process, but it wasn't. I asked ChatGPT to guide me through the process and it did, I just kept providing it with screenshots.</p>
<ol>
<li>Sign up or log in to the AWS account from here: <a rel="noreferrer" target="_blank" href="https://aws.amazon.com/ses/">https://aws.amazon.com/ses/</a></li>
<li>SES asks you to add and verify your email address and sending domain as well. For example, add and verify <code>[email protected]</code> and also your domain <code>yoursite.com</code>.</li>
<li>Keep other options in these sections as the default: Deliverability enhancements, Create your Dedicated IP pool, and Add tenant management. Review everything and click on Get started.</li>
<li>Verify your sending domain by adding a bunch of CNAME, DMARC, and SPF records at your domain registrar (or at Cloudflare if you're using it).</li>
<li>Now, create SMTP credentials and get the SMTP username, SMTP password, and AWS region (you'll need these inside Listmonk).</li>
</ol>
<p>I will suggest using ChatGPT, Gemini, or any other AI provider to guide you through everything.</p>
<h2>Setting up Listmonk</h2>
<p>Now, let's get through the process of setting up Listmonk:</p>
<h3>1. General settings</h3>
<p>First, you need to do some basic settings in the <strong>Settings > General</strong> tab, as you see in the screenshot here.</p>
<p><img alt="Listmonk general settings" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/listmonk-general-settings.png"></p>
<ul>
<li><strong>Site name</strong>: Add the name of your website where you will be showing the subscription form.</li>
<li><strong>Root URL</strong>: Add the root URL of where you have hosted Listmonk. Generally, people host at a subdomain of their main domain so the URL will be something like below
<ul>
<li><code>https://list.yoursite.com</code></li>
</ul>
</li>
<li><strong>Logo URL</strong>: This is the logo that is shown at the top of your emails, you can upload the logo to the media library and add the URL here.</li>
<li><strong>Favicon URL</strong>: This is shown as the favicon on the public archive pages, if you choose to make your issues public.</li>
<li><strong>Default <code>from</code> Email</strong>: Add your email that you want to send from.</li>
<li><strong>Admin notification emails</strong>: Add your email where you want to receive the notification when a campaign gets completed, and other admin related things.</li>
</ul>
<h2>2. SMTP settings</h2>
<p>You now have do the SMTP settings and you will need your Amazon SES credentials that you created earlier. See the below screenshot for more info.</p>
<p><img alt="Listmonk SMTP settings" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/listmonk-smtp-settings.png"></p>
<ul>
<li><strong>Enabled</strong>: Should be turned on.</li>
<li><strong>Host</strong>: You need to add this, but replace the AWS_REGION here:
<ul>
<li><code>email-smtp.AWS_REGION.amazonaws.com</code></li>
</ul>
</li>
<li><strong>Port</strong>: Use 587 as the SMTP server port.</li>
<li><strong>Auth protocol</strong>: Use LOGIN.</li>
<li><strong>Username</strong>: Paste the AWS SES username here.</li>
<li><strong>Password</strong>: Paste the AWS SES password here.</li>
<li><strong>HELO hostname</strong>: Leave it empty.</li>
<li><strong>TLS</strong>: Selet STARTTLS here.</li>
<li><strong>Name</strong>: Name anything you want.</li>
</ul>
<p>And keep other options as the default. You can see above screenshot for more info.</p>
<p>You can test the setup by clicking on the <strong>Test Connection</strong> on the same screen. And if your SES account is not verified yet, you will only be able to send the email to yourself (the one you added in the AWS SES account).</p>
<h3>3. Setting up the subscription form</h3>
<p>Inside Listmonk, go to the <strong>Lists > All lists</strong> and rename the default list to the name of your newsletter. This will be seen by the users in the welcome email. You can select between single and double opt-in. I like to have double opt-in, because it's good for deliverability and also that only serious people get into your list.</p>
<p><img alt="Creating a list in Listmonk" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/create-a-list.png"></p>
<p>Once the list is ready, navigate to <strong>Lists > Forms</strong> and you can either share the subscription form as the shown URL or get the HTML and embed it on your website.</p>
<p><img alt="Get the subscription form" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/subscription-form.png"></p>
<p>I have embedded the HTML on my website and it looks like this:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>newsletter-form<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>One email every week on AI, tech, and marketing finds I'd tell a friend about.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>form</span> <span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>post<span class="token punctuation">"</span></span> <span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://listmonk.yoursite.com/subscription/form<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>listmonk-form<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hidden<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>nonce<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hidden<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>l<span class="token punctuation">"</span></span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>9dcc7bc2<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>email<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>email<span class="token punctuation">"</span></span> <span class="token attr-name">required</span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Your email<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>name<span class="token punctuation">"</span></span> <span class="token attr-name">required</span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Your name<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>submit<span class="token punctuation">"</span></span> <span class="token attr-name">value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Subscribe<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>form</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>newsletter-note<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>No spam, just chaos.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span></code></pre>
<p>I have then added some CSS to make it look like how it currently looks in my website's footer. On mobile, form fields appear one above another.</p>
<h3>4. Setting up a campaign template</h3>
<p>I like sending plaintext emails so I had to create a new campaign template from <strong>Campaigns > Templates</strong>, but if you're fine with the default campaign template (you can see the preview), there's no need for this step.</p>
<blockquote>
<p>Actually, it's not absolutely plaintext because I want to use the HTML's hyperlink feature.</p>
</blockquote>
<p><img alt="Setting up the default Listmonk template" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/default-template.png"></p>
<p>As you can see, it's just basic HTML for adding hyperlink in <strong>Unsubscribe</strong> and <strong>View in browser</strong> links. I could use plaintext as well here, but the links look very ugly in the email.</p>
<h3>5. Sending a test campaign</h3>
<p>I recommend to subscribe to your own newsletter and then send a test campaign to make sure everything is working as expected. For sending a newsletter issue, go to <strong>Campaigns > All campaigns</strong>, delete the existing campaigns, and create a new one.</p>
<p><img alt="Creating a new campaign in Listmonk" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/create-new-campaign.png"></p>
<ul>
<li><strong>Name</strong>: Name it anything, it's internal and not shown to the subscribers.</li>
<li><strong>Subject</strong>: The email subject that you want subscribers to receive.</li>
<li><strong>Subject</strong>: From address that you added in Settings earlier and that you want to send the newsletter emails from.</li>
<li><strong>Lists</strong>: Select the list or lists of subscribers whom you want to send the emails to.</li>
<li><strong>Messenger</strong>: Keep as <code>email</code>.</li>
<li><strong>Format</strong>: Select Rich text as the format.</li>
<li><strong>Tags</strong>: Optional.</li>
<li><strong>Send later</strong>: If you want to schedule, use Send later option and select date and time to send. Otherwise, keep it turned off.</li>
</ul>
<p><img alt="Adding content for the new campaign" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/content-for-new-campaign.png"></p>
<p>Now, Save and go to the <strong>Content</strong> tab, select Rich text as the format, select either the default template or the new template you created previously, and start writing your newsletter.</p>
<p>As you see in the above screenshot, <code>.Subscriber.Name</code> between double curly braces will be replaced by the actual name of the subscriber. You can learn more about template expressions <a rel="noreferrer" target="_blank" href="https://listmonk.app/docs/templating/#template-expressions">from this page</a> in their docs.</p>
<p>After finishing writing, you can preview how it will look to the user, save changes, and start the campaign by clicking on the <strong>Start campaign</strong> button at the top.</p>
<p><img alt="Newsletter email received from Listmonk" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/email-received.png"></p>
<p>And seconds later, I received this test email in my Gmail account. It plaintext, mostly, and just uses HTML for the hyperlinks and will probably use for images (if I add).</p>
<p>That's it.</p>
<p>If you're setting Listmonk for yourself and got stuck somewhere, please email me and I would be happy to help.</p>
<p><strong>Update</strong>: Feb 4, 2026</p>
<p>Amazon has rejected my application for SES approval without giving any proper explanations, and I received this email from them.</p>
<p><img alt="Amazon rejected my SES application" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/aws-ses-rejected-email.png"></p>
<p>Now, with SES out of the question, I evaluated other SMTP options and have settled with this service called <a rel="noreferrer" target="_blank" href="https://www.smtp2go.com">SMTP2GO</a> for now. It offers 1,000 emails per month for free (with a limit of 200/day), and that's enough for me as of now, as I do not any subscribers yet.</p>
<p><img alt="Using Listmonk with SMTP2GO" decoding="async" src="https://assets.deepakness.com/blog/listmonk-setup/listmonk-with-smtp2go.png"></p>
<p>You can see my current Listmonk setup with SMTP2GO in the above screenshot. I tried subscribing to my newsletter from my other email, and it works well for now.</p>
Trying and Migrating to Vivaldi Browser2026-01-29T20:43:04Zhttps://deepakness.com/blog/vivaldi-browser/My experience of trying and migrating to the Vivaldi browser - things I liked and things I didn't.<p>I have known the Vivaldi browser for years but never actually tried it, until it was recently recommended by <a rel="noreferrer" target="_blank" href="https://x.com/elralfchristian/status/2016924706369147388">Ralf Christian on X</a>. Currently, I am using the <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/helium-browser/">Helium browser</a> for the last few months, and it works fine but lacks some features like Widevine DRM, translation, etc.</p>
<p>So... this time I actually decided to try the <a rel="noreferrer" target="_blank" href="https://vivaldi.com">Vivaldi browser</a>. I'm on macOS currently, so I downloaded the <code>.dmg</code> installation file (~220 MB in filesize) and installed it as you would any other app.</p>
<p>In this post, I will share my experience of using the browser – things I liked and also things I didn't.</p>
<h2>1. First impression</h2>
<p>After opening the browser for the first time, the onboarding flow was great the import from other browsers worked fine as well. But when the actual window opened, it was overwhelming as there were a lot of panels, settings, etc.</p>
<p>It took me almost an hour to tweak small things and the final look was like this:</p>
<p><img alt="Vivaldi light mode look on my computer" decoding="async" src="https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-first-look.png"></p>
<p>Here are the things that I liked and didn't like in the first hour of using the browser:</p>
<ol>
<li>Loved the different themes it provides by default and how customizable is everything.</li>
<li>Settings window was overwhelming as it has lots of options, but it's like this for the good reasons.</li>
<li>Loved the widgets on the home page and used the Date and Privacy Statistics ones.</li>
<li>Break Mode (ctrl + .) is amazing.</li>
<li>Different profiles was an expected feature, but Workspaces are an added benefit.</li>
<li>The inbuilt screenshot and page tiling feature.</li>
<li>Proper support for Chrome extensions, I could easily install one from Chrome webstore.</li>
<li>Vertical tabs, I'm not using it but great to have the feature.</li>
<li>A built-in Pomodoro timer? Amazing.</li>
<li>Support for the Manifest v2 Chrome extensions. I successfully installed this <a rel="noreferrer" target="_blank" href="https://chromewebstore.google.com/detail/scraper/mbigbapnjcgaffohmbkdlecaccepngjd">Scraper</a> extension.</li>
</ol>
<p><img alt="Vivaldi settings" decoding="async" src="https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-settings.png"></p>
<h2>2. Customizing as per my needs</h2>
<p>I changed the below settings from how the browser ships with the default settings:</p>
<ol>
<li>I don't like my tabs colors changing for different websites I visit, so I had to turn this off. But it was tricky, as searching "accent" in Settings doesn't show anything and I had to manually find this option under Themes > Editor > <strong>Accent from Page</strong>.</li>
<li>Removed the speed dials from new tabs, as I like to keep the new tab page clean. Currently, I'm just showing the Date and Privacy Statistics widgets on the Home.</li>
<li>Removed some items from the side panel, like some bookmarked websites, translation widget, some extensions that were shown, etc.</li>
<li>Turned off the <strong>Use Tab Zoom</strong> feature as it resets the zoom level when you re-open the website, and I wanted to retain the zoom level. I had to Google this one.</li>
</ol>
<p>I must have changed one or two more settings, but overall this was it.</p>
<h2>3. Things I loved</h2>
<p>Some features in the Vivaldi browser are amazing and not available in most other browser I tried so far.</p>
<h3>Secure sync feature</h3>
<p>I love a reliable sync feature which is also secure, and Vivaldi has it. It provides an end-to-end encrypted sync feature that I connect multiple devices to and share tabs, bookmarks, settings, history, and so on.</p>
<p><img alt="E2E sync feature in Vivaldi" decoding="async" src="https://assets.deepakness.com/blog/vivaldi-browser/e2e-sync-feature.png"></p>
<p>I connected my personal browser profile to the Vivaldi browser on my Android phone, and the sync immediately start working.</p>
<h3>Vivaldi for Android</h3>
<p>Initially, I didn't know that Vivaldi even has a browser for Android phones as well, but I was amazed when I installed it. It didn't have as many customization options like the desktop one, but</p>
<p><img alt="Vivaldi browser on Android" decoding="async" src="https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-on-android.png"></p>
<p>I only spent like 5 mins customizing and it already looks great on my phone. And the best thing now is, I can easily share tabs between my laptop and my phone by using <strong>Send to Your Devices</strong> feature.</p>
<h3>RSS feed reader</h3>
<p>When you visit a blog and it has an RSS feed, <a rel="noreferrer" target="_blank" href="https://vivaldi.com/features/feed-reader/">you see a small feed icon</a> in the right of the address bar and clicking on it opens a small dialog from where you can save the feed. And new posts will be automatically fetched on a regular interval for you to read.</p>
<p><img alt="RSS feed support in Vivaldi" decoding="async" src="https://assets.deepakness.com/blog/vivaldi-browser/rss-feed-support.png"></p>
<p>It works much like a fully functional feed reader app. I currently use an RSS reader app called <a rel="noreferrer" target="_blank" href="https://netnewswire.com">NetNewsWire</a>, but it's great feature in the browser.</p>
<p>By the way, there's also the Reader View that helps you read any articles without distractions. This feature is available in most browsers though.</p>
<h3>Page Actions</h3>
<p>One more crazy feature that I haven't seen in any browser is the Page Actions that lets you tweak any webpage for your advantage. Some cool actions are:</p>
<ul>
<li>Filter Black and White, Filter Grayscale, and even Filter Sepia</li>
<li>Fonts Monospace to convert the fonts on the webpage to monospace</li>
<li>Transition Removed to remove any kind of transitions</li>
</ul>
<p>... and much more that you can learn about <a rel="noreferrer" target="_blank" href="https://help.vivaldi.com/desktop/appearance-customization/page-actions/">from their documentation</a>.</p>
<h3>Mail and Calendar clients</h3>
<p>Vivaldi also has this stunning in-built <a rel="noreferrer" target="_blank" href="https://vivaldi.com/features/mail/">email</a> and <a rel="noreferrer" target="_blank" href="https://vivaldi.com/features/calendar/">calendar</a> clients that you can connect your Gmail, iCloud, Outlook, or basically any account that supports IMAP or POP3.</p>
<p><img alt="Mail and Calendar client in Vivaldi" decoding="async" src="https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-mail-calendar.png"></p>
<p>I connected my Google Workspace account just to see how it works, but I won't be using this feature as I prefer directly checking my emails via the web interface. But it's a great feature for people who do prefer an email/calendar client.</p>
<p>Or... maybe I will use this feature after I get more comfortable with the browser.</p>
<h2>4. Not so cool things</h2>
<p>But like any other things in the world, nothing is perfect. I didn't like a few things in the browser:</p>
<ol>
<li>Dragging a tab from my laptop screen to my second screen has some bugs. I tried dragging an already playing YouTube tab and then tab was lost – it's playing the background but not visible on either the first or second screen. I had to restart my browser to fix this issue.</li>
<li>Import feature doesn't work for different Chrome browser profiles. I tried importing my main profile and it worked, but I couldn't find a way to import other Chrome profiles. But it's not a deal-breaker, at least for me.</li>
</ol>
<p>I have completely migrated to the Vivaldi browser and have set it up as my default browser on both my laptop and my phone. It's been only a few hours since I installed it, but I am loving it so far.</p>
<p>I will keep this page updated, if I learn something new about the browser.</p>
<p><strong>Update</strong>: Jan 31, 2026</p>
<p>I turned off the drag-n-drop tab tiling feature by disabling the <strong>Enable Drag and Drop Tiling</strong> option under Settings > Tabs. It was buggy when I tried dragging away a tab into a new window or on my second screen (the same issue that I didn't like above).</p>
<p>It's working fine after turning this off, and I can still tile tabs by clicking on the Page Tiling option in the bottom panel.</p>
<p><strong>Update</strong>: Feb 01, 2026</p>
<p>I modified the below options from the Settings:</p>
<ul>
<li>Enabled Show Full Address from Settings > Address Bar > Address Field Options, as I like seeing the full URLs at the top.</li>
<li>Disabled Display Downloads Automatically from Settings > Downloads, as it was automatically opening the left side panel whenever I downloaded something.</li>
<li>Disabled Prioritize Bookmarks from Settings > Address Bar > Address Field Suggestions, as it was always over-prioritizing bookmarks even after opening another website with a similar name multiple times.</li>
</ul>
<p><strong>Update</strong>: Feb 03, 2026</p>
<p>The #1 bug I talked about in the above "not so cool things" section, has become really annoying, so I reported it to the Vivaldi team and hopefully they do something about it. I haven't received any response from them yet, but this is the bug I reported.</p>
<p><img alt="Vivaldi Tab dragging bug reported" decoding="async" src="https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-bug-reported.png"></p>
<p>Currently, I am not dragging tabs out of the current window because this is super buggy. And since dragging windows work fine, I set up a keyboard shortcut <code>cmd + ctrl + n</code> to move tab to a new window. Now, I can immediately detach a tab from a window to a new window and then drag the new window to my second screen easily.</p>
<p><img alt="Move a tab to a new window" decoding="async" src="https://assets.deepakness.com/blog/vivaldi-browser/move-tab-to-a-new-window.png"></p>
<p>To set this up, go to Settings > Keyboard, expand Tab toggle, and press whatever combination you want in front of Move Tab to New Window option.</p>
<p>To be honest, I am liking the keyboard shortcut method much better than dragging the tab away.</p>
Better Handling Images on My 11ty Blog2026-01-27T17:57:12Zhttps://deepakness.com/blog/better-images/Better image handling on my blog and pre-optimizing them all instead of using @11ty/eleventy-img plugin.<p>Last year I <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/from-wordpress-to-11ty/">migrated away from WordPress</a> and now using 11ty SSG for my blog <code>deepakness.com</code> and while I was mostly happy with everything, I have also occasionally <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1994638454026899806">had second thoughts</a> about using 11ty. And mainly because of the increasing Netlify build times as my blog grew, it was taking up to 1 min 23 seconds for a build. Once or twice I even hit my Netlify monthly 300 mins build time limits.</p>
<p><img alt="Old Netlify build time" decoding="async" src="https://assets.deepakness.com/blog/better-images/old-netlify-build-time.png"></p>
<p>So I had to find a better way for the site, and I had the following two options in my mind:</p>
<ol>
<li>Ditch 11ty and start using WordPress or something similar</li>
<li>Stay on 11ty and optimize the build time on Netlify</li>
</ol>
<p>I went with the #2 option as it required much less work than migrating back to WordPress, and started looking into what's causing the build times to increase so much.</p>
<p>And it was the <a rel="noreferrer" target="_blank" href="https://www.11ty.dev/docs/plugins/image/">@11ty/eleventy-img plugin</a> that I was using to dynamically optimize and transform images during build time. While it worked great, it wasted so much compute as it ran image transformations during each build. And I tried using <a rel="noreferrer" target="_blank" href="https://github.com/11ty/demo-eleventy-img-netlify-cache/blob/main/netlify.toml">netlify-plugin-cache for caching</a> already transformed images, but it didn't properly work because I am co-locating all images in separate blog folders, as you see below:</p>
<pre class="language-plaintext" tabindex="0"><code class="language-plaintext">blog/
└── some-post/
├── index.md
├── image-1.png
└── image-2.png</code></pre>
<p>So... what was the solution here?</p>
<p>I removed the <code>@11ty/eleventy-img</code> plugin and decided to locally process all images using the <code>sharp</code> <a rel="noreferrer" target="_blank" href="https://sharp.pixelplumbing.com/">image library</a>, before sending them to Netlify because the build times will be faster then.</p>
<p>This worked as expected, and here's what I am doing with images:</p>
<ol>
<li>Resize original images to 1600px, compress, and keep them in their original format</li>
<li>Create a <code>.webp</code> version of images (1200px wide), and save with the same filename</li>
<li>Track already optimized images in the <code>.image-cache.json</code> file via MD5 hash</li>
</ol>
<p>And all this happens with the following script I created:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span> fs <span class="token keyword">from</span> <span class="token string">'fs'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> path <span class="token keyword">from</span> <span class="token string">'path'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> crypto <span class="token keyword">from</span> <span class="token string">'crypto'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> sharp <span class="token keyword">from</span> <span class="token string">'sharp'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> fileURLToPath <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'url'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> __filename <span class="token operator">=</span> <span class="token function">fileURLToPath</span><span class="token punctuation">(</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> __dirname <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">dirname</span><span class="token punctuation">(</span>__filename<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">ROOT_DIR</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">resolve</span><span class="token punctuation">(</span>__dirname<span class="token punctuation">,</span> <span class="token string">'..'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">BLOG_DIR</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">ROOT_DIR</span><span class="token punctuation">,</span> <span class="token string">'content'</span><span class="token punctuation">,</span> <span class="token string">'blog'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token constant">CACHE_FILE</span> <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">ROOT_DIR</span><span class="token punctuation">,</span> <span class="token string">'.image-cache.json'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Configuration</span>
<span class="token keyword">const</span> <span class="token constant">CONFIG</span> <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">webp</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">maxWidth</span><span class="token operator">:</span> <span class="token number">1200</span><span class="token punctuation">,</span> <span class="token literal-property property">quality</span><span class="token operator">:</span> <span class="token number">80</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">original</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">maxWidth</span><span class="token operator">:</span> <span class="token number">1600</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">extensions</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'.jpg'</span><span class="token punctuation">,</span> <span class="token string">'.jpeg'</span><span class="token punctuation">,</span> <span class="token string">'.png'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token comment">// Load or create cache</span>
<span class="token keyword">function</span> <span class="token function">loadCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span><span class="token constant">CACHE_FILE</span><span class="token punctuation">,</span> <span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">function</span> <span class="token function">saveCache</span><span class="token punctuation">(</span><span class="token parameter">cache</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
fs<span class="token punctuation">.</span><span class="token function">writeFileSync</span><span class="token punctuation">(</span><span class="token constant">CACHE_FILE</span><span class="token punctuation">,</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>cache<span class="token punctuation">,</span> <span class="token keyword">null</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Calculate file hash</span>
<span class="token keyword">function</span> <span class="token function">getFileHash</span><span class="token punctuation">(</span><span class="token parameter">filePath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> content <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span>filePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> crypto<span class="token punctuation">.</span><span class="token function">createHash</span><span class="token punctuation">(</span><span class="token string">'md5'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">update</span><span class="token punctuation">(</span>content<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">digest</span><span class="token punctuation">(</span><span class="token string">'hex'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Find all images in blog folders</span>
<span class="token keyword">function</span> <span class="token function">findImages</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> images <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span><span class="token constant">BLOG_DIR</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Blog directory not found:'</span><span class="token punctuation">,</span> <span class="token constant">BLOG_DIR</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> images<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> postDirs <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readdirSync</span><span class="token punctuation">(</span><span class="token constant">BLOG_DIR</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">withFileTypes</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span><span class="token parameter">d</span> <span class="token operator">=></span> d<span class="token punctuation">.</span><span class="token function">isDirectory</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">d</span> <span class="token operator">=></span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token constant">BLOG_DIR</span><span class="token punctuation">,</span> d<span class="token punctuation">.</span>name<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> postDir <span class="token keyword">of</span> postDirs<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> files <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readdirSync</span><span class="token punctuation">(</span>postDir<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> file <span class="token keyword">of</span> files<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> ext <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">extname</span><span class="token punctuation">(</span>file<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token constant">CONFIG</span><span class="token punctuation">.</span>extensions<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span>ext<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token operator">!</span>file<span class="token punctuation">.</span><span class="token function">endsWith</span><span class="token punctuation">(</span><span class="token string">'.webp'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
images<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>postDir<span class="token punctuation">,</span> file<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> images<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Optimize a single image</span>
<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">optimizeImage</span><span class="token punctuation">(</span><span class="token parameter">imagePath<span class="token punctuation">,</span> cache</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> relativePath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">relative</span><span class="token punctuation">(</span><span class="token constant">ROOT_DIR</span><span class="token punctuation">,</span> imagePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> hash <span class="token operator">=</span> <span class="token function">getFileHash</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Skip if already optimized with same hash</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>cache<span class="token punctuation">[</span>relativePath<span class="token punctuation">]</span><span class="token operator">?.</span>hash <span class="token operator">===</span> hash<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">⏭️ Skipping (unchanged): </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>relativePath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> dir <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">dirname</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> ext <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">extname</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> basename <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">basename</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">,</span> ext<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> webpPath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>dir<span class="token punctuation">,</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>basename<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.webp</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> image <span class="token operator">=</span> <span class="token function">sharp</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> metadata <span class="token operator">=</span> <span class="token keyword">await</span> image<span class="token punctuation">.</span><span class="token function">metadata</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Create WebP version (max 1200px, quality 80)</span>
<span class="token keyword">let</span> webpPipeline <span class="token operator">=</span> <span class="token function">sharp</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> finalWidth <span class="token operator">=</span> metadata<span class="token punctuation">.</span>width<span class="token punctuation">;</span>
<span class="token keyword">let</span> finalHeight <span class="token operator">=</span> metadata<span class="token punctuation">.</span>height<span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>metadata<span class="token punctuation">.</span>width <span class="token operator">></span> <span class="token constant">CONFIG</span><span class="token punctuation">.</span>webp<span class="token punctuation">.</span>maxWidth<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> ratio <span class="token operator">=</span> <span class="token constant">CONFIG</span><span class="token punctuation">.</span>webp<span class="token punctuation">.</span>maxWidth <span class="token operator">/</span> metadata<span class="token punctuation">.</span>width<span class="token punctuation">;</span>
finalWidth <span class="token operator">=</span> <span class="token constant">CONFIG</span><span class="token punctuation">.</span>webp<span class="token punctuation">.</span>maxWidth<span class="token punctuation">;</span>
finalHeight <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">round</span><span class="token punctuation">(</span>metadata<span class="token punctuation">.</span>height <span class="token operator">*</span> ratio<span class="token punctuation">)</span><span class="token punctuation">;</span>
webpPipeline <span class="token operator">=</span> webpPipeline<span class="token punctuation">.</span><span class="token function">resize</span><span class="token punctuation">(</span><span class="token constant">CONFIG</span><span class="token punctuation">.</span>webp<span class="token punctuation">.</span>maxWidth<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">await</span> webpPipeline
<span class="token punctuation">.</span><span class="token function">webp</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">quality</span><span class="token operator">:</span> <span class="token constant">CONFIG</span><span class="token punctuation">.</span>webp<span class="token punctuation">.</span>quality <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">toFile</span><span class="token punctuation">(</span>webpPath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Optimize original (max 1600px, keep format)</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>metadata<span class="token punctuation">.</span>width <span class="token operator">></span> <span class="token constant">CONFIG</span><span class="token punctuation">.</span>original<span class="token punctuation">.</span>maxWidth<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> tempPath <span class="token operator">=</span> imagePath <span class="token operator">+</span> <span class="token string">'.tmp'</span><span class="token punctuation">;</span>
<span class="token keyword">await</span> <span class="token function">sharp</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">resize</span><span class="token punctuation">(</span><span class="token constant">CONFIG</span><span class="token punctuation">.</span>original<span class="token punctuation">.</span>maxWidth<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">toFile</span><span class="token punctuation">(</span>tempPath<span class="token punctuation">)</span><span class="token punctuation">;</span>
fs<span class="token punctuation">.</span><span class="token function">renameSync</span><span class="token punctuation">(</span>tempPath<span class="token punctuation">,</span> imagePath<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Recalculate dimensions after resize</span>
<span class="token keyword">const</span> newMeta <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">sharp</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">metadata</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
finalWidth <span class="token operator">=</span> newMeta<span class="token punctuation">.</span>width<span class="token punctuation">;</span>
finalHeight <span class="token operator">=</span> newMeta<span class="token punctuation">.</span>height<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Update cache with dimensions</span>
cache<span class="token punctuation">[</span>relativePath<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token literal-property property">hash</span><span class="token operator">:</span> <span class="token function">getFileHash</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token comment">// Recalculate after optimization</span>
<span class="token literal-property property">width</span><span class="token operator">:</span> finalWidth<span class="token punctuation">,</span>
<span class="token literal-property property">height</span><span class="token operator">:</span> finalHeight<span class="token punctuation">,</span>
<span class="token literal-property property">optimizedAt</span><span class="token operator">:</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">✅ Optimized: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>relativePath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> (</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>finalWidth<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">x</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>finalHeight<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">)</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">❌ Error optimizing </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>relativePath<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">:</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> error<span class="token punctuation">.</span>message<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token comment">// Main function</span>
<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'\n🖼️ Starting image optimization...\n'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> cache <span class="token operator">=</span> <span class="token function">loadCache</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> images <span class="token operator">=</span> <span class="token function">findImages</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Found </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>images<span class="token punctuation">.</span>length<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> images to check\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> optimized <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> skipped <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> imagePath <span class="token keyword">of</span> images<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> wasOptimized <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">optimizeImage</span><span class="token punctuation">(</span>imagePath<span class="token punctuation">,</span> cache<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>wasOptimized<span class="token punctuation">)</span> optimized<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token keyword">else</span> skipped<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">saveCache</span><span class="token punctuation">(</span>cache<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">\n✨ Done! Optimized: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>optimized<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">, Skipped: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>skipped<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">\n</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">catch</span><span class="token punctuation">(</span>console<span class="token punctuation">.</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>The script optimizes the original image, creates a WebP version of the image, and adds updates the <code>.image-cache.json</code> file to avoid re-optimization of the image(s). The script also adds the width and height of the images in the JSON cache file to be later used for <code>width</code> and <code>height</code> in the <code>img</code> tags to avoid any <a rel="noreferrer" target="_blank" href="https://web.dev/articles/cls">cumulative layout shift</a> on the pages.</p>
<p><img alt="Local image optimization using sharp library" decoding="async" src="https://assets.deepakness.com/blog/better-images/local-image-optimization.png"></p>
<p>And after this my Netlify build time was reduced to just 20 seconds, as you see in the screenshot below:</p>
<p><img alt="New Netlify build time" decoding="async" src="https://assets.deepakness.com/blog/better-images/new-netlify-build-time.png"></p>
<p>And I use the HTML <code><picture></code> tag to show images as you see below.</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>picture</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>source</span> <span class="token attr-name">srcset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./image.webp<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/webp<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>alt text<span class="token punctuation">"</span></span> <span class="token attr-name">decoding</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>async<span class="token punctuation">"</span></span> <span class="token attr-name">width</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1200<span class="token punctuation">"</span></span> <span class="token attr-name">height</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>630<span class="token punctuation">"</span></span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./image.png<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>picture</span><span class="token punctuation">></span></span></code></pre>
<p>The browser first tries to load the <code>.webp</code> version of the image, and <a rel="noreferrer" target="_blank" href="https://caniuse.com/webp">if it's not supported</a> then loads the optimized version in the original format (PNG/JPEG). I only link to the original image in my Markdown file as <code></code> and the above <code>picture</code> tag with WebP is automatically added in the final HTML via the following code in the <code>eleventy.config.js</code> file:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"> <span class="token comment">// Picture tag transform: wrap <img> with <picture> for WebP + fallback</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">addTransform</span><span class="token punctuation">(</span><span class="token string">"pictureTag"</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">content<span class="token punctuation">,</span> outputPath</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// Only apply to blog pages</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>outputPath <span class="token operator">||</span> <span class="token operator">!</span>outputPath<span class="token punctuation">.</span><span class="token function">endsWith</span><span class="token punctuation">(</span><span class="token string">".html"</span><span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token operator">!</span>outputPath<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">"/blog/"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> content<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Load image dimensions cache</span>
<span class="token keyword">let</span> imageCache <span class="token operator">=</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> cacheContent <span class="token operator">=</span> fs<span class="token punctuation">.</span><span class="token function">readFileSync</span><span class="token punctuation">(</span><span class="token string">'.image-cache.json'</span><span class="token punctuation">,</span> <span class="token string">'utf-8'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
imageCache <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>cacheContent<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">{</span>
<span class="token comment">// Cache not found, continue without dimensions</span>
<span class="token punctuation">}</span>
<span class="token comment">// Extract blog slug from outputPath (e.g., "_site/blog/better-images/index.html" -> "better-images")</span>
<span class="token keyword">const</span> blogMatch <span class="token operator">=</span> outputPath<span class="token punctuation">.</span><span class="token function">match</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\/blog\/([^/]+)\/</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> blogSlug <span class="token operator">=</span> blogMatch <span class="token operator">?</span> blogMatch<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">:</span> <span class="token keyword">null</span><span class="token punctuation">;</span>
<span class="token comment">// Match <img> tags with jpg/jpeg/png src</span>
<span class="token keyword">return</span> content<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span>
<span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex"><img\s+([^>]*?)src=["']([^"']+\.(jpg|jpeg|png))["']([^>]*)></span><span class="token regex-delimiter">/</span><span class="token regex-flags">gi</span></span><span class="token punctuation">,</span>
<span class="token punctuation">(</span><span class="token parameter">match<span class="token punctuation">,</span> before<span class="token punctuation">,</span> src<span class="token punctuation">,</span> ext<span class="token punctuation">,</span> after</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token comment">// Skip if already inside a <picture> tag</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>match<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">'data-no-picture'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> match<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Create WebP source path</span>
<span class="token keyword">const</span> webpSrc <span class="token operator">=</span> src<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\.(jpg|jpeg|png)$</span><span class="token regex-delimiter">/</span><span class="token regex-flags">i</span></span><span class="token punctuation">,</span> <span class="token string">'.webp'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Keep existing attributes, add decoding async if not present</span>
<span class="token keyword">let</span> attrs <span class="token operator">=</span> before <span class="token operator">+</span> after<span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>attrs<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">'decoding='</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
attrs <span class="token operator">+=</span> <span class="token string">' decoding="async"'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Add width/height from cache if available</span>
<span class="token comment">// Convert relative src (e.g., "./image.png") to cache key (e.g., "content/blog/slug/image.png")</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>blogSlug <span class="token operator">&&</span> <span class="token operator">!</span>attrs<span class="token punctuation">.</span><span class="token function">includes</span><span class="token punctuation">(</span><span class="token string">'width='</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> imageName <span class="token operator">=</span> src<span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">^\.\/</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// Remove leading ./</span>
<span class="token keyword">const</span> cacheKey <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">content/blog/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>blogSlug<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>imageName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token keyword">const</span> cached <span class="token operator">=</span> imageCache<span class="token punctuation">[</span>cacheKey<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>cached<span class="token operator">?.</span>width <span class="token operator">&&</span> cached<span class="token operator">?.</span>height<span class="token punctuation">)</span> <span class="token punctuation">{</span>
attrs <span class="token operator">+=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> width="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>cached<span class="token punctuation">.</span>width<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" height="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>cached<span class="token punctuation">.</span>height<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">return</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"><picture>
<source srcset="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>webpSrc<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">" type="image/webp">
<img </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>attrs<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> src="</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>src<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">">
</picture></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>It's a bit complicated setup, but I am very much satisfied with the setup, and will stay with 11ty, for now.</p>
Creating My First WordPress Plugin2025-12-21T17:10:54Zhttps://deepakness.com/blog/first-wordpress-plugin/I created my very first WordPress plugin, and now it's live on the WordPress.org repository.<p>I created my very first <a rel="noreferrer" target="_blank" href="https://wordpress.org/plugins/bulk-classic-to-block/">WordPress plugin called Bulk Classic to Block</a>, and now it's live on the WordPress.org repository. It was a fun process, but definitely wasn't straightforward as it took me almost a month to get the <code>v1.0.0</code> live.</p>
<p>But I'm ready to explain the entire thing in detail in this blog post, so let's directly get to it.</p>
<h2>1. The idea for the plugin</h2>
<p>I had a really old WordPress website which was untouched for years and was still using the Classic Editor, and since there were 100s of pages on the site, it wasn't manually possible to convert each page to Blocks. So... I then got this idea of creating such a WordPress plugin which does this in bulk.</p>
<p>Initially, I didn't know if such a thing will even work or not, but I researched a bit and then started working on the plugin. I used Cursor AI and Google's Antigravity for developing this.</p>
<h2>2. Initial submission to WordPress.org</h2>
<p>I, first, submitted the plugin to <a rel="noreferrer" target="_blank" href="https://wordpress.org/plugins/developers/">WordPress.org</a> on November 29, 2025 and received the following email immediately after that.</p>
<p><img alt="Initial submission of the WordPress plugin" decoding="async" src="https://assets.deepakness.com/blog/first-wordpress-plugin/initial-submission.png"></p>
<p>And then I heard nothing from them for a few days.</p>
<h2>3. Plugin checked by a volunteer</h2>
<p>13 days later, on December 8, 2025, I received the below email saying that a volunteer has checked my plugin and has identified some issues that need to be corrected and that I needed to resubmit the updated version.</p>
<p><img alt="WordPress plugin checked by a volunteer" decoding="async" src="https://assets.deepakness.com/blog/first-wordpress-plugin/plugin-checked-by-volunteer.png"></p>
<p>Mainly, the issues were related to the following 2 things (along with the solutions suggested):</p>
<ul>
<li>Use wp_enqueue commands
<ul>
<li>wp_register_script() and wp_enqueue_script() to add JavaScript code from a file.</li>
<li>wp_add_inline_script() to add inline JavaScript code to previous declared scripts.</li>
<li>wp_register_style() and wp_enqueue_style() to add CSS from a file.</li>
<li>wp_add_inline_style() to add inline CSS to previously declared CSS.</li>
</ul>
</li>
<li>Nonces and User Permissions Needed for Security
<ul>
<li>Please add a nonce check to your input calls ($_POST, $_GET, $REQUEST) to prevent unauthorized access.</li>
</ul>
</li>
</ul>
<h2>4. Updated and re-submitted the plugin</h2>
<p>I then updated the plugin as per the detailed suggestions from the volunteer and then re-submitted it for the approval on the same day. And also sent them this email as it was suggested in the previous email.</p>
<p><img alt="Updated the WordPress plugin and sent this confirmation email" decoding="async" src="https://assets.deepakness.com/blog/first-wordpress-plugin/updated-the-plugin.png"></p>
<p>And then I waited and was expecting of some more issues flagged from the volunteer.</p>
<h2>5. The plugin gets approved</h2>
<p>To my surprise, today I received this email from WordPress saying that the plugin is now approved. I was happy that now it should be live, but then visiting this URL <code>https://wordpress.org/plugins/bulk-classic-to-block/</code> showed nothing.</p>
<p><img alt="Bulk Classic to Block plugin gets approved on WordPress.org" decoding="async" src="https://assets.deepakness.com/blog/first-wordpress-plugin/plugin-gets-approved.png"></p>
<p>So... I went through the email again, this time reading every line, to discover that now I have to upload the approved code to WordPress Plugin Directory via SVN. This information was sent in another email that you see below.</p>
<p><img alt="Uploading plugin to WordPress Plugin Directory via SVN" decoding="async" src="https://assets.deepakness.com/blog/first-wordpress-plugin/uploading-the-plugin-via-svn.png"></p>
<p>I started learning about <a rel="noreferrer" target="_blank" href="https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/">using SVN for WordPress</a> and then got some understanding that it's version control system, much like <a rel="noreferrer" target="_blank" href="https://git-scm.com/">Git</a>. Also learned that it can be used via CLI as well as via some GUI clients like <a rel="noreferrer" target="_blank" href="https://tortoisesvn.net/">TortoiseSVN</a> and <a rel="noreferrer" target="_blank" href="https://www.smartsvn.com/">SmartSVN</a>, again, much like Git.</p>
<h2>6. Uploading the plugin</h2>
<p>I decided to use the CLI for using <code>svn</code> (Subversion) because what's even the fun in using this via the GUI. At first, I had to install this on my computer, and I am using macOS so decided to install <code>svn</code> via <a rel="noreferrer" target="_blank" href="https://formulae.brew.sh/formula/subversion">Homebrew</a> by running the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">brew <span class="token function">install</span> subversion</code></pre>
<p>The idea here was to pull the public WordPress.org repo of my plugin in a folder locally, copy the plugin files in the specified folders, and then push everything live. The code files (images) went in the <code>trunk</code> folder and then screenshots and icons went in the <code>assets</code> folder. Below are the <code>svn</code> commands I had to execute:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token comment"># Executed at first</span>
svn checkout https://plugins.svn.wordpress.org/bulk-classic-to-block
<span class="token builtin class-name">cd</span> bulk-classic-to-block
<span class="token comment"># After copying files to `trunk` and `assets` folders as specified above</span>
svn status
svn <span class="token function">add</span> trunk/* assets/* <span class="token parameter variable">--force</span>
svn commit <span class="token parameter variable">-m</span> <span class="token string">"Initial commit of plugin files"</span>
svn copy trunk tags/1.0.0
svn commit <span class="token parameter variable">-m</span> <span class="token string">"Tag version 1.0.0"</span></code></pre>
<p>If you'd like, here's the entire text copied from my terminal.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">deepak@m2air wordpress-org % svn checkout https://plugins.svn.wordpress.org/bulk-classic-to-block
A bulk-classic-to-block/assets
A bulk-classic-to-block/tags
A bulk-classic-to-block/trunk
Checked out revision <span class="token number">3424796</span>.
deepak@m2air wordpress-org % <span class="token builtin class-name">cd</span> bulk-classic-to-block
deepak@m2air bulk-classic-to-block % <span class="token function">ls</span>
assets tags trunk
deepak@m2air bulk-classic-to-block % svn status
? assets/screenshot-1.png
? assets/screenshot-2.png
? assets/screenshot-3.png
? trunk/LICENSE.txt
? trunk/README.txt
? trunk/bulk-classic-to-block.php
? trunk/index.php
? trunk/js
deepak@m2air bulk-classic-to-block % svn <span class="token function">add</span> trunk/* assets/* <span class="token parameter variable">--force</span>
A trunk/bulk-classic-to-block.php
A trunk/index.php
A trunk/js
A trunk/js/scripts.js
A trunk/LICENSE.txt
A trunk/README.txt
A <span class="token punctuation">(</span>bin<span class="token punctuation">)</span> assets/screenshot-1.png
A <span class="token punctuation">(</span>bin<span class="token punctuation">)</span> assets/screenshot-2.png
A <span class="token punctuation">(</span>bin<span class="token punctuation">)</span> assets/screenshot-3.png
deepak@m2air bulk-classic-to-block % svn status
A assets/screenshot-1.png
A assets/screenshot-2.png
A assets/screenshot-3.png
A trunk/LICENSE.txt
A trunk/README.txt
A trunk/bulk-classic-to-block.php
A trunk/index.php
A trunk/js
A trunk/js/scripts.js
deepak@m2air bulk-classic-to-block % svn commit <span class="token parameter variable">-m</span> <span class="token string">"Initial commit of plugin files"</span>
Authentication realm: <span class="token operator"><</span>https://plugins.svn.wordpress.org:44<span class="token operator"><span class="token file-descriptor important">3</span>></span> Use your WordPress.org login
Password <span class="token keyword">for</span> <span class="token string">'deepak'</span><span class="token builtin class-name">:</span> ********************************************
Authentication realm: <span class="token operator"><</span>https://plugins.svn.wordpress.org:44<span class="token operator"><span class="token file-descriptor important">3</span>></span> Use your WordPress.org login
Username: deepakness
Password <span class="token keyword">for</span> <span class="token string">'deepakness'</span><span class="token builtin class-name">:</span> ********************************************
Adding <span class="token punctuation">(</span>bin<span class="token punctuation">)</span> assets/screenshot-1.png
Adding <span class="token punctuation">(</span>bin<span class="token punctuation">)</span> assets/screenshot-2.png
Adding <span class="token punctuation">(</span>bin<span class="token punctuation">)</span> assets/screenshot-3.png
Adding trunk/LICENSE.txt
Adding trunk/README.txt
Adding trunk/bulk-classic-to-block.php
Adding trunk/index.php
Adding trunk/js
Adding trunk/js/scripts.js
Transmitting <span class="token function">file</span> data <span class="token punctuation">..</span><span class="token punctuation">..</span><span class="token punctuation">..</span><span class="token punctuation">..</span>done
Committing transaction<span class="token punctuation">..</span>.
Committed revision <span class="token number">3424811</span>.
deepak@m2air bulk-classic-to-block % svn copy trunk tags/1.0.0
A tags/1.0.0
deepak@m2air bulk-classic-to-block % svn commit <span class="token parameter variable">-m</span> <span class="token string">"Tag version 1.0.0"</span>
Adding tags/1.0.0
Adding tags/1.0.0/LICENSE.txt
Adding tags/1.0.0/README.txt
Adding tags/1.0.0/bulk-classic-to-block.php
Adding tags/1.0.0/index.php
Adding tags/1.0.0/js
Committing transaction<span class="token punctuation">..</span>.
Committed revision <span class="token number">3424816</span>.
deepak@m2air bulk-classic-to-block %</code></pre>
<p>And then my WordPress plugin <strong>Bulk Classic to Block</strong> was live, and I could access it from this URL: <a rel="noreferrer" target="_blank" href="https://wordpress.org/plugins/bulk-classic-to-block/">https://wordpress.org/plugins/bulk-classic-to-block/</a></p>
<h2>7. Future updates</h2>
<p>If I update anything to the plugin files in the future, I might need to run the following commands:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">cd</span> ~/bulk-classic-to-block
<span class="token comment"># copy or edit files</span>
svn <span class="token function">add</span> assets/screenshot-4.png
<span class="token comment"># or edit readme.txt</span>
svn commit <span class="token parameter variable">-m</span> <span class="token string">"Update screenshots and readme"</span></code></pre>
<p>Above is when I add a screenshot or a minor change without need to bump up the version, but when I have to do a new release then the process will be:</p>
<ol>
<li>Make changes in files in the /trunk folder</li>
<li>Bump the version number in:
<ol>
<li>Main plugin file</li>
<li>readme.txt file</li>
<li>Changelog section</li>
</ol>
</li>
<li>Commit /trunk by using the <code>svn commit -m "message"</code> command</li>
<li>Create a new tag (important) by running the following commands:</li>
</ol>
<pre class="language-bash" tabindex="0"><code class="language-bash">svn copy trunk tags/1.0.1
svn commit <span class="token parameter variable">-m</span> <span class="token string">"Tag 1.0.1"</span></code></pre>
<p>Also, some other helpful <code>svn</code> commands can be:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token comment"># See what changed</span>
svn status
<span class="token comment"># See diff</span>
svn <span class="token function">diff</span>
<span class="token comment"># Update local copy</span>
svn update</code></pre>
<p>That's it!</p>
<p>I wrote everything in detail here, because I would definitely need to look back at it in the future when I update the plugin or even when I have to publish a new plugin to WordPress.</p>
<p><strong>Update: 22 Jan, 2026</strong></p>
<p>I made the GitHub repository for the plugin open-source that <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/wp-bulk-classic-to-block">you can access here</a>. Now, you can either download the plugin from <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/wp-bulk-classic-to-block/releases">GitHub releases</a> or directly install via the <a rel="noreferrer" target="_blank" href="https://wordpress.org/plugins/bulk-classic-to-block/">official WordPress.org repo</a>.</p>
<p>Will keep adding plugin updates to this page in the future.</p>
Upgrading Self-hosted Umami to v3 on a VPS2025-11-19T17:24:51Zhttps://deepakness.com/blog/upgrading-umami-v3/A quick guide to upgrading self-hosted Umami to the latest v3 on a Hetzner VPS.<p>I recently wrote about <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/self-hosting-umami-analytics">self-hosting Umami Analytics</a> on a small Hetzner VPS using Docker and Caddy.</p>
<p>Since then, <a rel="noreferrer" target="_blank" href="https://umami.is/blog/umami-v3">Umami shipped v3</a> and I wanted to upgrade my existing install (v2.19.0) without losing any data. In this post, I am sharing the exact steps I followed on my live server.</p>
<p>If you installed Umami by following <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/self-hosting-umami-analytics">my earlier post</a>, this guide should be a direct drop-in upgrade for you.</p>
<h2>1. Log in to the VPS</h2>
<p>From my local machine (macOS), I first SSHed into the VPS:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">ssh</span> root@your_server_ip</code></pre>
<p>The server is running Ubuntu 24.04 with Docker CE, same as before.</p>
<p>After logging in I noticed a pending kernel update and a restart notice, so I rebooted first to avoid surprises in the middle of the upgrade:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">reboot</span></code></pre>
<p>Then I connected again:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">ssh</span> root@your_server_ip</code></pre>
<h2>2. Confirm the current Umami setup</h2>
<p>Once logged in, I checked what files are in the root directory:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">ls</span></code></pre>
<p>Output:</p>
<pre class="language-text" tabindex="0"><code class="language-text">Caddyfile docker-compose.yml</code></pre>
<p>So I opened the <code>docker-compose.yml</code> to confirm the current setup:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">cat</span> docker-compose.yml</code></pre>
<p>The important part looks like this:</p>
<p><img alt="Umami Docker Compose file contents" decoding="async" src="https://assets.deepakness.com/blog/upgrading-umami-v3/umami-docker-compose-file.png"></p>
<p>This is the same Docker setup I used in the original installation post:</p>
<ul>
<li><code>umami</code> container using <code>postgresql-latest</code> image</li>
<li><code>db</code> container running <code>postgres:15-alpine</code></li>
<li><code>caddy</code> in front, handling HTTPS and the domain</li>
</ul>
<p>To make sure everything was running fine before touching anything, I checked the containers:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose <span class="token function">ps</span></code></pre>
<p>Sample output:</p>
<pre class="language-text" tabindex="0"><code class="language-text">NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
root-caddy-1 caddy:latest "caddy run --config …" caddy 4 weeks ago Up 2 hours 0.0.0.0:80->80/tcp, ...
root-db-1 postgres:15-alpine "docker-entrypoint.s…" db 4 weeks ago Up 2 hours (healthy) 5432/tcp
root-umami-1 ghcr.io/umami-software/umami:postgresql-latest "docker-entrypoint.s…" umami 4 weeks ago Up 2 hours (healthy) 3000/tcp</code></pre>
<p>All good here.</p>
<h2>3. Create a PostgreSQL backup (do not skip this)</h2>
<p>Before changing any image or running migrations, I created a full dump of the Umami database.</p>
<p>Since the <code>db</code> service is using:</p>
<ul>
<li><code>POSTGRES_DB=umami</code></li>
<li><code>POSTGRES_USER=umami</code></li>
</ul>
<p>the backup command is:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose <span class="token builtin class-name">exec</span> db pg_dump <span class="token parameter variable">-U</span> umami umami <span class="token operator">></span> umami-backup-<span class="token variable"><span class="token variable">$(</span><span class="token function">date</span> +%F-%H%M<span class="token variable">)</span></span>.sql</code></pre>
<p>This runs <code>pg_dump</code> inside the <code>db</code> container and saves the file on the host with a timestamp in the name.</p>
<p>Then I double checked that the backup file exists and has some size:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">ls</span> <span class="token parameter variable">-lh</span> umami-backup-*.sql</code></pre>
<p>Output on my server:</p>
<pre class="language-text" tabindex="0"><code class="language-text">-rw-r--r-- 1 root root 8.5M Nov 19 14:48 umami-backup-2025-11-19-1448.sql</code></pre>
<p>8.5 MB looks right for my current traffic.</p>
<p>At this point, even if the migration failed, I can restore the DB using this SQL file. That gives a lot of peace of mind.</p>
<h2>4. Update the Docker image to Umami v3.0.1</h2>
<p>Next, I edited <code>docker-compose.yml</code> to point to the new v3 image.</p>
<p>I used <code>nano</code>:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">nano</span> docker-compose.yml</code></pre>
<p>Inside the file, in the <code>umami</code> service, <strong>I changed this line</strong>:</p>
<pre class="language-yml" tabindex="0"><code class="language-yml"><span class="token key atrule">image</span><span class="token punctuation">:</span> ghcr.io/umami<span class="token punctuation">-</span>software/umami<span class="token punctuation">:</span>postgresql<span class="token punctuation">-</span>latest</code></pre>
<p><strong>to this</strong>:</p>
<pre class="language-yml" tabindex="0"><code class="language-yml"><span class="token key atrule">image</span><span class="token punctuation">:</span> ghcr.io/umami<span class="token punctuation">-</span>software/umami<span class="token punctuation">:</span>3.0.1</code></pre>
<blockquote>
<p>Nothing else needed to change – the <code>DATABASE_URL</code>, <code>DATABASE_TYPE</code>, <code>APP_SECRET</code>, and the <code>db</code> service all stay the same.</p>
</blockquote>
<p>After saving and closing nano (<code>Ctrl+X</code>, <code>Y</code>, <code>Enter</code>), I confirmed the change with:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">grep</span> <span class="token parameter variable">-A3</span> <span class="token string">'umami:'</span> docker-compose.yml</code></pre>
<p>Output:</p>
<pre class="language-text" tabindex="0"><code class="language-text"> umami:
image: ghcr.io/umami-software/umami:3.0.1
environment:
DATABASE_URL: postgresql://umami:umami@db:5432/umami</code></pre>
<p>Perfect.</p>
<h2>5. Pull the new image</h2>
<p>Now that the compose file points to <code>3.0.1</code>, I pulled the new image:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose pull umami</code></pre>
<p>This downloads the <code>ghcr.io/umami-software/umami:3.0.1</code> image. My output looked like this (trimmed):</p>
<pre class="language-text" tabindex="0"><code class="language-text">[+] Pulling 18/18
✔ umami Pulled 16.0s
✔ fc2cca81d0de Pull complete 2.4s
✔ ...
✔ 14e4cc53c863 Pull complete 14.8s </code></pre>
<p>Once this finished, the image was ready on the server.</p>
<h2>6. Recreate the Umami container with v3</h2>
<p>With the image pulled, I restarted just the <code>umami</code> service using the new image:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose up <span class="token parameter variable">-d</span> --force-recreate umami</code></pre>
<p>Output:</p>
<pre class="language-text" tabindex="0"><code class="language-text">[+] Running 2/2
✔ Container root-db-1 Healthy 0.8s
✔ Container root-umami-1 Started 1.1s </code></pre>
<p>Note that the <code>db</code> container stays the same, including the data volume. We are only replacing the app container.</p>
<h2>7. Watch the logs and wait for migrations</h2>
<p>The first start after the image change will run database checks and migrations. To see what is happening in real time, I tailed the logs:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose logs <span class="token parameter variable">-f</span> umami</code></pre>
<p>The logs showed:</p>
<pre class="language-text" tabindex="0"><code class="language-text">umami-1 | > [email protected] start-docker /app
umami-1 | > npm-run-all check-db update-tracker start-server
umami-1 |
umami-1 | > [email protected] check-db /app
umami-1 | > node scripts/check-db.js
umami-1 |
umami-1 | ✓ DATABASE_URL is defined.
umami-1 | ✓ Database connection successful.
umami-1 | ✓ Database version check successful.
umami-1 | Prisma schema loaded from prisma/schema.prisma
umami-1 | Datasource "db": PostgreSQL database "umami", schema "public" at "db:5432"
umami-1 |
umami-1 | 14 migrations found in prisma/migrations
umami-1 |
umami-1 | Applying migration `14_add_link_and_pixel`
umami-1 |
umami-1 | The following migration(s) have been applied:
umami-1 | └─ 14_add_link_and_pixel/
umami-1 | └─ migration.sql
umami-1 |
umami-1 | All migrations have been successfully applied.
umami-1 |
umami-1 | ✓ Database is up to date.
umami-1 |
umami-1 | > [email protected] start-server /app
umami-1 | > node server.js
umami-1 |
umami-1 | ▲ Next.js 15.5.3
umami-1 | - Local: http://localhost:3000
umami-1 | - Network: http://0.0.0.0:3000
umami-1 |
umami-1 | ✓ Starting...
umami-1 | ✓ Ready in 485ms</code></pre>
<p>This is exactly what you want to see:</p>
<ul>
<li>DB connection successful</li>
<li>Migrations applied</li>
<li>Server starting and ready</li>
</ul>
<p>Once I saw the app was ready, I pressed <code>Ctrl + C</code> to stop tailing the logs.</p>
<h2>8. Confirm that Umami v3 is live</h2>
<p>With the container running, I opened my Umami domain in the browser (the one I pointed to Caddy in the first post).</p>
<p>The login page loaded as usual. After logging in:</p>
<ul>
<li>The dashboard still showed all the historical data</li>
<li>The list of websites was intact</li>
<li>New pageviews were being tracked without changing the tracking script</li>
</ul>
<p>If something looked broken here, I still had my SQL backup file to fall back to, but in my case everything worked on the first try.</p>
<h2>9. What to do if something breaks</h2>
<p>If the logs show errors, or the app keeps restarting in a loop, here is a quick recovery plan:</p>
<ol>
<li>
<p>Stop the stack:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose down</code></pre>
</li>
<li>
<p>Bring only the database back up:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose up <span class="token parameter variable">-d</span> db</code></pre>
</li>
<li>
<p>Restore the backup:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">cat</span> umami-backup-YYYY-MM-DD-HHMM.sql <span class="token operator">|</span> <span class="token function">docker</span> compose <span class="token builtin class-name">exec</span> <span class="token parameter variable">-T</span> db psql <span class="token parameter variable">-U</span> umami umami</code></pre>
<p>Replace the file name with your backup file name.</p>
</li>
<li>
<p>Change the image back to the old one in <code>docker-compose.yml</code>:</p>
<pre class="language-yml" tabindex="0"><code class="language-yml"><span class="token key atrule">image</span><span class="token punctuation">:</span> ghcr.io/umami<span class="token punctuation">-</span>software/umami<span class="token punctuation">:</span>postgresql<span class="token punctuation">-</span>latest</code></pre>
</li>
<li>
<p>Start Umami again:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose up <span class="token parameter variable">-d</span> umami</code></pre>
</li>
</ol>
<p>You should now be back on the old version with the old data.</p>
<h2>10. Final notes</h2>
<p>That is pretty much all I had to do to upgrade Umami from the <code>postgresql-latest</code> v2 image to <code>3.0.1</code>:</p>
<ul>
<li>One SSH session</li>
<li>One <code>pg_dump</code> backup</li>
<li>One image tag change</li>
<li>A <code>pull</code>, an <code>up</code>, and a quick log check</li>
</ul>
<p>If you are running the same Docker + Caddy setup <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/self-hosting-umami-analytics">from my earlier post</a>, you can follow the same steps line by line, just replace the IP and domain with yours.</p>
<p>If you try this upgrade and hit any issues, the logs and the SQL backup will be your best friends – check those first before touching anything else.</p>
Self-hosting Umami Analytics on a VPS2025-10-20T02:58:00Zhttps://deepakness.com/blog/self-hosting-umami-analytics/A quick guide to self-hosting Umami Analytics on a $3.50 Hetzner VPS via Docker.<p>A few days ago, I <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/self-hosting-plausible-analytics">self-hosted Plausible analytics</a> and <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1979566150335008797">many people recommended</a> that I should check Umami analytics as it's lightweight and updates are more frequent.</p>
<p>So here it is... I also self-hosted <a rel="noreferrer" target="_blank" href="https://github.com/umami-software/umami/">Umami analytics</a> on a $3.50 Hetzner VPS to see if it actually lives up to the hype. First, I will explain the installation steps and then write about my experience.</p>
<h2>Installing Umami on a Hetzner VPS</h2>
<p>First, you need to have a Hetzner account where you can create new servers. If you do not have one, create one and then get yourself verified, if required (yes, I had to submit my ID a few years ago).</p>
<p>I also recorded a quick video showing everything from the start – from setting up the VPS to adding the website for tracking.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="efPDmAiYMKY" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="efPDmAiYMKY" style="background-image: url('https://i.ytimg.com/vi/efPDmAiYMKY/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>But if you prefer reading, keep going forward.</p>
<h3>1. Set up the VPS</h3>
<p>Create a new server, even the smallest shared server is enough to host Umami as <a rel="noreferrer" target="_blank" href="https://lowendtalk.com/discussion/comment/4536502/#Comment_4536502">people discussed</a> that only 500 MB RAM is enough to efficiently run it.</p>
<p>Also, I'm not going to explain how to set up a new server on Hetzner so if you need help with that you can <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/self-hosting-plausible-analytics">go through this post</a>, as I have explained the process a bit.</p>
<blockquote>
<p>And as explained, <strong>set up a Docker CE app</strong> instead of setting up a blank system as we'll be installing Umami via Docker.</p>
</blockquote>
<p>Now, connect to the server via SSH from your terminal by running <code>ssh root@your_ip_address</code> command, and once connected, update the system by running the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> update <span class="token operator">&&</span> <span class="token function">sudo</span> <span class="token function">apt</span> upgrade</code></pre>
<p>And it's ready for the installation.</p>
<h3>2. Umami and Caddy setup via Docker</h3>
<p>While you can directly follow the official <a rel="noreferrer" target="_blank" href="https://github.com/umami-software/umami/?tab=readme-ov-file#-installing-with-docker">installing with Docker</a> steps, I have a better way of doing it by also using Caddy for also setting up the custom domain.</p>
<p>For this, you need to create a <code>docker-compose.yml</code> file in the root by running the following <code>nano</code> command.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">nano</span> docker-compose.yml</code></pre>
<p>Now, just copy-paste the below YML code in the file, then save and exit by pressing <code>ctrl + x</code> and then <code>y</code>. But replace <code>replace-me-with-a-random-string</code> with a string before pasting; I prefer running below openssl command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">openssl rand <span class="token parameter variable">-base64</span> <span class="token number">32</span></code></pre>
<p>And then use the generated string below:</p>
<pre class="language-yml" tabindex="0"><code class="language-yml"><span class="token key atrule">services</span><span class="token punctuation">:</span>
<span class="token key atrule">umami</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> ghcr.io/umami<span class="token punctuation">-</span>software/umami<span class="token punctuation">:</span>postgresql<span class="token punctuation">-</span>latest
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token key atrule">DATABASE_URL</span><span class="token punctuation">:</span> postgresql<span class="token punctuation">:</span>//umami<span class="token punctuation">:</span>umami@db<span class="token punctuation">:</span>5432/umami
<span class="token key atrule">DATABASE_TYPE</span><span class="token punctuation">:</span> postgresql
<span class="token key atrule">APP_SECRET</span><span class="token punctuation">:</span> to4KhI0g8Wzaw1r7+vEknSbU4AM4d6LRJyGzlgO5DZY=
<span class="token key atrule">depends_on</span><span class="token punctuation">:</span>
<span class="token key atrule">db</span><span class="token punctuation">:</span>
<span class="token key atrule">condition</span><span class="token punctuation">:</span> service_healthy
<span class="token key atrule">init</span><span class="token punctuation">:</span> <span class="token boolean important">true</span>
<span class="token key atrule">restart</span><span class="token punctuation">:</span> always
<span class="token key atrule">healthcheck</span><span class="token punctuation">:</span>
<span class="token key atrule">test</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">"CMD-SHELL"</span><span class="token punctuation">,</span> <span class="token string">"curl http://localhost:3000/api/heartbeat"</span><span class="token punctuation">]</span>
<span class="token key atrule">interval</span><span class="token punctuation">:</span> 5s
<span class="token key atrule">timeout</span><span class="token punctuation">:</span> 5s
<span class="token key atrule">retries</span><span class="token punctuation">:</span> <span class="token number">5</span>
<span class="token key atrule">db</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> postgres<span class="token punctuation">:</span>15<span class="token punctuation">-</span>alpine
<span class="token key atrule">environment</span><span class="token punctuation">:</span>
<span class="token key atrule">POSTGRES_DB</span><span class="token punctuation">:</span> umami
<span class="token key atrule">POSTGRES_USER</span><span class="token punctuation">:</span> umami
<span class="token key atrule">POSTGRES_PASSWORD</span><span class="token punctuation">:</span> umami
<span class="token key atrule">volumes</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> umami<span class="token punctuation">-</span>db<span class="token punctuation">-</span>data<span class="token punctuation">:</span>/var/lib/postgresql/data
<span class="token key atrule">restart</span><span class="token punctuation">:</span> always
<span class="token key atrule">healthcheck</span><span class="token punctuation">:</span>
<span class="token key atrule">test</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token string">"CMD-SHELL"</span><span class="token punctuation">,</span> <span class="token string">"pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"</span><span class="token punctuation">]</span>
<span class="token key atrule">interval</span><span class="token punctuation">:</span> 5s
<span class="token key atrule">timeout</span><span class="token punctuation">:</span> 5s
<span class="token key atrule">retries</span><span class="token punctuation">:</span> <span class="token number">5</span>
<span class="token key atrule">caddy</span><span class="token punctuation">:</span>
<span class="token key atrule">image</span><span class="token punctuation">:</span> caddy<span class="token punctuation">:</span>latest
<span class="token key atrule">restart</span><span class="token punctuation">:</span> unless<span class="token punctuation">-</span>stopped
<span class="token key atrule">ports</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token string">"80:80"</span>
<span class="token punctuation">-</span> <span class="token string">"443:443"</span>
<span class="token punctuation">-</span> <span class="token string">"443:443/udp"</span>
<span class="token key atrule">depends_on</span><span class="token punctuation">:</span>
<span class="token key atrule">umami</span><span class="token punctuation">:</span>
<span class="token key atrule">condition</span><span class="token punctuation">:</span> service_started
<span class="token key atrule">volumes</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> ./Caddyfile<span class="token punctuation">:</span>/etc/caddy/Caddyfile<span class="token punctuation">:</span>ro
<span class="token punctuation">-</span> caddy_data<span class="token punctuation">:</span>/data
<span class="token punctuation">-</span> caddy_config<span class="token punctuation">:</span>/config
<span class="token key atrule">volumes</span><span class="token punctuation">:</span>
<span class="token key atrule">umami-db-data</span><span class="token punctuation">:</span>
<span class="token key atrule">caddy_data</span><span class="token punctuation">:</span>
<span class="token key atrule">caddy_config</span><span class="token punctuation">:</span></code></pre>
<p>Now, create a <code>Caddyfile</code> by running the below command and paste the below Caddyfile code, but first replace the <code>your_domain_or_subdomain_here</code> with your domain/subdomain.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">nano</span> Caddyfile</code></pre>
<p>For example, I can connect it to <code>umami.deepakness.com</code>. Again, press <code>ctrl + x</code> and then <code>y</code> to save and exit.</p>
<pre class="language-caddyfile" tabindex="0"><code class="language-caddyfile">your_domain_or_subdomain_here {
encode zstd gzip
reverse_proxy {
to umami:3000
health_uri /api/heartbeat
health_interval 10s
health_timeout 2s
}
log {
output stdout
format json
}
}</code></pre>
<p>Also, you will need to add a DNS record at Cloudflare (if you're using) or at your domain registrar. You need to add an <code>A</code> record with your IP address.</p>
<p><img alt="Adding a DNS record in Cloudflare." decoding="async" src="https://assets.deepakness.com/blog/self-hosting-umami-analytics/cloudflare-dns-record.png"></p>
<p>For example, I use Cloudflare and this is how I'd add the <code>A</code> DNS record in there.</p>
<h3>3. Start Docker containers</h3>
<p>Now, everything is ready, and you can just start the docker container by running the below command. It starts all the containers in the background.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose up <span class="token parameter variable">-d</span></code></pre>
<p>And then you can also run the below command (optional) to see live logs from the Caddy container so you can see if it gets the SSL certificate and starts properly.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose logs <span class="token parameter variable">-f</span> caddy</code></pre>
<p>And your Umami Analytics instance should be live!</p>
<p>You can visit the domain/subdomain and should see the login screen, and your default username will be <code>admin</code> and password <code>umami</code>. But you should <a rel="noreferrer" target="_blank" href="https://umami.is/docs/login">immediately change your password</a> after logging in.</p>
<p>And that's it.</p>
<p>You can add your website(s) for tracking and <a rel="noreferrer" target="_blank" href="https://umami.is/docs/collect-data">start collecting data</a> as explained in their docs.</p>
<h2>My experience with Umami</h2>
<p>Actually, I <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1979566150335008797">liked Umami a lot</a>, it's better than Plausible in terms of server resource usage. You can see the usage here for both:</p>
<p><strong>Plausible is using 1 GB of RAM:</strong></p>
<p><img alt="Plausible htop RAM usage" decoding="async" src="https://assets.deepakness.com/blog/self-hosting-umami-analytics/plausible-htop.png"></p>
<p><strong>Umami is using ~500 MB of RAM:</strong></p>
<p><img alt="Umami htop RAM usage" decoding="async" src="https://assets.deepakness.com/blog/self-hosting-umami-analytics/umami-htop.png"></p>
<p>Apart from this, I also liked the Umami dashboard a lot as it shows more tracking data while consuming 50% less resources than Plausible. Also, Plausible keeps showing a huge footer like below on all pages which I didn't like, but it's not there in Umami.</p>
<p><img alt="Unnecessary Plausible footer" decoding="async" src="https://assets.deepakness.com/blog/self-hosting-umami-analytics/plausible-footer.png"></p>
<p>Apart from this, I liked the data points being shown in Umami, especially the way sessions and events data is shown.</p>
<p><img alt="Sessions data in Umami" decoding="async" src="https://assets.deepakness.com/blog/self-hosting-umami-analytics/umami-sessions.png"></p>
<p>So... that's it.</p>
<p>This time, I am going with Umami as it seemed more polished to me.</p>
<p><strong>Update: November 19, 2025</strong></p>
<p>I upgraded Umami to v3.0.1 and it was a smooth process. Check out <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/upgrading-umami-v3">my post on upgrading Umami to v3</a> for more details. I have explained the process in detail.</p>
Self-hosting Plausible on a Hetzner VPS2025-10-18T13:19:01Zhttps://deepakness.com/blog/self-hosting-plausible-analytics/A quick guide to self-hosting Plausible Community Edition on a 3.50 USD Hetzner VPS.<p>Previously, I have always used Google Analytics to track visitors on my website, but this time, I decided to use Plausible analytics on <a rel="noreferrer" target="_blank" href="https://deepakness.com/">my personal website</a>. And mainly because of the following three reasons:</p>
<ol>
<li>Plausible is lightweight so doesn't affect the website loading speed much</li>
<li>Unlike Google Analytics, Plausible is open-source and privacy-friendly, and</li>
<li>The Plausible dashboard is user-friendly and shows essential data without complexity</li>
</ol>
<p>Now, let's directly go to the self-hosting part. I will explain each and every step of self-hosting <a rel="noreferrer" target="_blank" href="https://github.com/plausible/community-edition/">Plausible CE</a> on a Hetzner VPS.</p>
<h2>1. Set up a Hetzner VPS</h2>
<p>If you don't have a Hetzner account, create one and get yourself verified by submitting an ID proof (I don't know if that's for everyone, but did have to submit). After logging in, go to <code>console.hetzner.com</code> page, create a Project and then go inside the Project to create a Server.</p>
<p>Click on the big <strong>Add Server</strong> button, and you will land on the below page. In this case, select the <strong>Cost-Optimized x86 (Intel®/AMD)</strong> version and then the <strong>CX23</strong> server as shown in the screenshot. And this is more than enough to run Plausible, as it only <a rel="noreferrer" target="_blank" href="https://github.com/plausible/community-edition/?tab=readme-ov-file#prerequisites">requires 2GB of RAM</a> to run efficiently, and we have 4GB here.</p>
<p><img alt="Create a new server on Hetzner" decoding="async" src="https://assets.deepakness.com/blog/self-hosting-plausible-analytics/hetzner-create-server.jpeg"></p>
<p>Below are the recommendations for other options on the page:</p>
<ul>
<li><strong>Location</strong>: Leave the location to Helsinki as cheaper servers are available here.</li>
<li><strong>Image</strong>: Instead of choosing OS Images, go to the <strong>Apps</strong> section and choose <strong>Docker CE</strong>.</li>
<li><strong>Networking</strong>: Make sure IPv4 and IPv6 are selected, they should already be selected.</li>
<li><strong>SSH Keys</strong>: If you use SSH keys on your computer, I'd recommend using these for easier access later. Maybe look it up online on how to set it up for your OS.</li>
<li>Leave Volumes, Firewalls, Backups, Placement Groups, Labels, and Cloud config as they are. No need to write anything here.</li>
<li><strong>Name</strong>: Give your server a recognizable name for future references. You can always change it later.</li>
</ul>
<p>Click on the <strong>Create and Buy Now</strong> button and your server will be ready in a few minutes.</p>
<h2>2. Install Plausible on the server</h2>
<p>Now, you will have to connect to the server from your local Terminal and then install the Plausible CE on the server. To connect, you need to copy the IPv4 from the server that just got created, and run the following command from your terminal:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">ssh</span> root@YOUR_IPv4_ADDRESS</code></pre>
<p>If you have the server connected via SSH, you just have press Enter again, and it will be connected. Otherwise, you might need to enter the server root password.</p>
<blockquote>
<p>If you don't choose the SSH method, you will receive the root password via email.</p>
</blockquote>
<p>After your terminal gets connect to the server, you can update the server by running the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> update <span class="token operator">&&</span> <span class="token function">sudo</span> <span class="token function">apt</span> upgrade</code></pre>
<p>It should look like below:</p>
<p><img alt="Update and upgrade the Hetzner server" decoding="async" src="https://assets.deepakness.com/blog/self-hosting-plausible-analytics/update-hetzner-server.png"></p>
<p>After this completes, go to the <a rel="noreferrer" target="_blank" href="https://github.com/plausible/community-edition/">Plausible community edition GitHub repo</a> and follow the instructions. First, you need to run the following command to clone the repo (but make sure to check the latest command from the repo):</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> clone <span class="token parameter variable">-b</span> v3.0.1 --single-branch https://github.com/plausible/community-edition plausible-ce</code></pre>
<p>Now, go the <code>plausible-ce</code> folder by running below command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">cd</span> plausible-ce</code></pre>
<p>Create a <code>.env</code> file by running the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">touch</span> .env</code></pre>
<p>And then add <code>BASE_URL</code> and <code>SECRET_KEY_BASE</code> by running the following commands:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"BASE_URL=https://plausible.example.com"</span> <span class="token operator">>></span> .env</code></pre>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"SECRET_KEY_BASE=<span class="token variable"><span class="token variable">$(</span>openssl rand <span class="token parameter variable">-base64</span> <span class="token number">48</span><span class="token variable">)</span></span>"</span> <span class="token operator">>></span> .env</code></pre>
<p>Make sure to replace <code>BASE_URL</code> with the URL where you want to host Plausible. You will also need to add an <code>A</code> DNS record with your server IP at Cloudflare (if you're using) or at your domain registrar.</p>
<blockquote>
<p>You can also manually add <code>BASE_URL</code> and <code>SECRET_KEY_BASE</code> by running the <code>nano .env</code> command in the terminal.</p>
</blockquote>
<p>Now, run the below commands one by one:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"HTTP_PORT=80"</span> <span class="token operator">>></span> .env</code></pre>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">echo</span> <span class="token string">"HTTPS_PORT=443"</span> <span class="token operator">>></span> .env</code></pre>
<p>And the run the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">cat</span> <span class="token operator">></span> compose.override.yml <span class="token operator"><<</span> <span class="token string">EOF
services:
plausible:
ports:
- 80:80
- 443:443
EOF</span></code></pre>
<p>Finally, it's time to start the services with Docker Compose by running the below command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">docker</span> compose up <span class="token parameter variable">-d</span></code></pre>
<p>And you will see something like below in the terminal:</p>
<p><img alt="Start Docker services for Plausible" decoding="async" src="https://assets.deepakness.com/blog/self-hosting-plausible-analytics/start-docker-services.jpeg"></p>
<p>Now, it's time to create user and add your website for tracking.</p>
<blockquote>
<p>I have created a short <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=efPDmAiYMKY">video for self-hosting Umami</a>, so some steps will be the same when installing Plausible.</p>
</blockquote>
<h2>3. Start tracking visitors</h2>
<p>First, you will need to create a user by visiting the BASE_URL you set earlier, something like <code>https://plausible.example.com</code> and create an account with your credentials.</p>
<p>And when asked, add the domain you want to track, something like <code>deepakness.com</code>, and go to the next step. You can enable optional measurements as shown below, and then <strong>Copy</strong> the script that's being shown in the box.</p>
<p><img alt="Adding a new site to Plausible" decoding="async" src="https://assets.deepakness.com/blog/self-hosting-plausible-analytics/add-new-site-plausible.jpeg"></p>
<p>Now, go to your website and add the script somewhere between the <code><head>...</head></code> tags on your website, <a rel="noreferrer" target="_blank" href="https://plausible.io/docs/plausible-script">as explained in their docs</a>. If you're having difficulties, you can also <a rel="noreferrer" target="_blank" href="https://plausible.io/docs/integration-guides">check this page</a> for different installation options for websites with different tech-stacks.</p>
<p>Lastly, click on the <strong>Start collecting data</strong> button, and you should be good to go within a few minutes. Once correctly installed, you'll start seeing the data being shown in the dashboard.</p>
<p>And the best thing is, you can track users on multiple websites from the single Plausible dashboard.</p>
<blockquote>
<p>While I have provided all the commands here in the post, I recommend checking on the <a rel="noreferrer" target="_blank" href="https://github.com/plausible/community-edition">Plausible community edition repo</a> in case something changes.</p>
</blockquote>
<p>That's it.</p>
<blockquote>
<p>I also <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/self-hosting-umami-analytics">self-hosted Umami analytics</a> as it was recommended by a lot of folks. In the post, you can also see a short comparison between the both.</p>
</blockquote>
<p>Hope this helps.</p>
Trying and Migrating to the Helium Browser2025-10-03T02:08:51Zhttps://deepakness.com/blog/helium-browser/My experience of completely migrating to the Helium browser, from Brave browser.<p>I was using the Brave browser for a few years now, but was also trying different browsers like <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/trying-ungoogled-chromium/">Ungoogled Chromium</a> and <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/trying-librewolf-browser/">LibreWolf</a> in the search of something better. Honestly, I didn't know what I was looking for in my web browser, but I just kept trying them.</p>
<p>However, I kept coming back to the Brave browser as it has everything that others lack.</p>
<p>But this time, I have completely migrated to the <a rel="noreferrer" target="_blank" href="https://helium.computer/">Helium browser</a>, and for the good only. The browser was recommended to me <a rel="noreferrer" target="_blank" href="https://x.com/fardeenxyz/status/1971179056352932029">by Fardeen on X</a>, and I <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1971526673498833244">installed it the next day</a> after I saw multiple people mentioning online.</p>
<p>It's been over a week since I'm using the <a rel="noreferrer" target="_blank" href="https://github.com/imputnet/helium">open-source Helium</a> as my default browser now, and I've never been happier using a <a rel="noreferrer" target="_blank" href="https://deepakness.com/tags/web-browser/">web browser</a>. In this post, I will be sharing my experience – the good, bad, and everything.</p>
<h2>1. Clean and quick</h2>
<p>When I first installed, I didn't see any annoying bloatware at all. It looked very clean and minimal on the very first install, and it didn't even have the background image set on new tabs. Unlike the Brave browser, it <strong>doesn't have</strong>:</p>
<ul>
<li>sponsored backgrounds</li>
<li>crypto bloats</li>
<li>unnecessary AI push</li>
</ul>
<p>Below, take a look at how clean it looks.</p>
<p><img alt="Clean look of the Helium browser" decoding="async" src="https://assets.deepakness.com/blog/helium-browser/clean-helium-browser.jpeg"></p>
<p>And the migration process was quicker than I thought, as I was able to import my Brave browser profiles within seconds – from history to bookmarks and even the extensions I could import everything.</p>
<h2>2. Based on Chromium</h2>
<p>To be honest, I've always liked Chromium engine more than that of any other browser, and the Helium browser is based on Chromium project itself. It's fast and supports all Chrome browser extensions as well.</p>
<p>In fact, it also supports the <strong>Manifest v2</strong> extensions and comes with uBlock Origin preinstalled. And manifest v2 is a big deal for me because I want extensions like uBlock Origin, <a rel="noreferrer" target="_blank" href="https://chromewebstore.google.com/detail/scraper/mbigbapnjcgaffohmbkdlecaccepngjd">Scraper</a>, and more correctly working for me.</p>
<h2>3. Safe and secure</h2>
<p>The Helium browser comes with all the good features of the Brave browser in terms of security and also without the tracking capabilities of the Google Chrome browser.</p>
<p>I also noticed that it doesn't save your addresses, card details, passwords, etc. as these features are completely removed from the browser. And I like this a lot because on Brave or Chrome, I always first turned off these features when getting started.</p>
<p>But it still does have the "Use secure DNS" feature which I absolutely need, as I use the <a rel="noreferrer" target="_blank" href="https://one.one.one.one/">Cloudflare's 1.1.1.1</a> DNS resolver.</p>
<h2>4. Cool features</h2>
<p>While I don't use extra features a lot, but they're good to have, and I like them so far. For example:</p>
<ul>
<li>!bangs are awesome</li>
<li>Split View is handy sometimes</li>
</ul>
<p>I also like that it lets me completely customize the top toolbar. By the way, this option appears when you right-click anywhere on the toolbar.</p>
<p>There's <strong>one minor inconvenience</strong> of using the browser is that it can't play DRM protected content in the browser, means, you won't be able to watch Netflix or Amazon Prime in the browser. And it makes sense because <a rel="noreferrer" target="_blank" href="https://github.com/imputnet/helium/issues/116">Helium doesn't have Widevine DRM</a>, as it costs tens of thousands of dollars to get. But it's not a deal-breaker for me, as I rarely watch DRM content on my computer and even if I sometimes do, I still have Brave installed that I can use.</p>
<p>Apart from this, I'm absolutely loving the browser and sticking with it.</p>
<p>Also, it'd be great if the browser was available for mobile devices as well, but I don't think that's possible as of now.</p>
<p><strong>Update</strong>: Oct 8, 2025</p>
<p>I just noticed that the Helium browser also doesn't have the translation feature as if you're visiting a page that's not in the language you know, you can't translate. But that's not a big deal for me, I can use some browser extension for that when needed.</p>
<p><strong>Update</strong>: Nov 5, 2025</p>
<p>Now, it's been more than a month, and I'm still sticking with the Helium browser. I love how clean it is and how there are no distracting elements in the browser. The browser has received 1-2 updates since the installation, but I didn't notice any major changes – everything works as expected.</p>
<p>The browser is very stable and definitely consumes less RAM and resources than the Chrome and even the Brave browser. In the past one month, I had to open the Brave browser for one-time to translate a webpage (as Helium doesn't support translation), otherwise it's good so far.</p>
<p><strong>Update</strong>: Jan 30, 2026</p>
<p>I loved the Helium browser as it was my default browser for almost 4 months, but I have recently <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/vivaldi-browser/">migrated to the Vivaldi browser</a>.</p>
macOS App to Show Lid Angle in the Menu Bar2025-09-11T08:51:46Zhttps://deepakness.com/blog/show-lid-angle-in-menu-bar/A macOS app that keeps showing the current angle of the MacBook screen or lid in the menu bar.<p>I came across <a rel="noreferrer" target="_blank" href="https://x.com/samhenrigold/status/1964428927159382261">this cool project</a> where Sam was able to access a hidden API from macOS which output the current angle of the lid. He built a cool app that accessed the API and showed the angle, as <a rel="noreferrer" target="_blank" href="https://x.com/samhenrigold/status/1964428927159382261">shown in the video here</a>.</p>
<p>I was inspired by the project a lot and decided to build my first macOS app – an app that just shows the current tilt angle of the lid/screen in the menu bar. Here's a <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=0DoqINQkjO4">quick demo video</a> showing the same.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="0DoqINQkjO4" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="0DoqINQkjO4" style="background-image: url('https://i.ytimg.com/vi/0DoqINQkjO4/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>I also <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1966061409562931599">posted about the same on X</a> along with the <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/LidAngle">public GitHub repo</a> that you can clone and run the app via XCode on your MacBook devices.</p>
<p>To build this, I set up a macOS app project in XCode and then used OpenAI Codex to build the app for me. It took several iterations and also <a rel="noreferrer" target="_blank" href="https://www.tomshardware.com/software/developer-tools/engineer-taps-into-apples-hidden-lidanglesensor-api-to-create-a-creaky-door-simulator">some manual researching and reading</a> about the hidden <strong>LidAngleSensor API</strong> and how it can be accessed. And then it finally worked.</p>
<p><img alt="Building LidAngle app in XCode" decoding="async" src="https://assets.deepakness.com/blog/show-lid-angle-in-menu-bar/lidangle-xcode.png"></p>
<p>I still don't know how to get the final executable file from here, but I am reading about it and should also release the <code>.dmg</code> file that one can use to directly install this with just a click.</p>
<blockquote>
<p>I have added some more info about how this works and limitations <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/LidAngle/blob/main/README.md">in the README.md file</a> on GitHub. You can learn some interesting things about the API and the app from there.</p>
</blockquote>
<p>Will keep updating the post as I discover more.</p>
Git and GitHub for Vibe Coders2025-09-09T18:09:56Zhttps://deepakness.com/blog/git-for-vibe-coders/Everything you need to know about Git and GitHub as a vibe coder.<blockquote>
<p>This post is <strong>written using AI</strong> (unlike my other posts). Sometimes, I revisit some of these git commands and it was meant to be a private note, but then I published it because others might also find this useful.</p>
</blockquote>
<p>I've been coding with AI tools like Cursor, Claude Code, and OpenAI Codex for a while now, and let me tell you – it's both amazing and terrifying. One moment you're asking AI to "add user authentication to this app," and the next moment you're staring at 200 lines of generated code wondering if it just broke your entire project.</p>
<p>And this is called <strong>vibe coding</strong> (<a rel="noreferrer" target="_blank" href="https://x.com/karpathy/status/1886192184808149383">coined by Andrej Karpathy</a>) – using AI to generate code based on prompts and vibes rather than writing every line yourself. It's incredibly powerful, but it can also turn your codebase into chaos if you're not careful.</p>
<p>That's where Git and GitHub come in. Think of them as your safety net when AI coding goes sideways.</p>
<h2>Why vibe coders desperately need Git</h2>
<p>Here's the thing about vibe coding: you often don't fully understand what the AI just generated. Sure, it looks right, and it might even work, but what happens when you realize it broke something else? Or when you want to undo that "small refactor" that turned into a complete rewrite?</p>
<blockquote>
<p>Without Git, you're basically coding without a parachute.</p>
</blockquote>
<p>Git is essentially a time machine for your code. Every commit is a snapshot you can return to, and every branch is a parallel universe where you can experiment without consequences.</p>
<h2>Git vs GitHub</h2>
<p>I used to think Git and GitHub were the same thing. Spoiler alert: they're not.</p>
<ul>
<li><strong>Git</strong> is the actual version control system that runs on your computer. It works completely offline and tracks all your changes locally. You can use Git without ever touching the internet.</li>
<li><strong>GitHub</strong> is a website that hosts your Git repositories online. It's like Google Drive for your code repositories, but with extra features like Issues, Pull Requests, and Actions.</li>
</ul>
<p>Think of it this way: Git is like having a really good filing system for your documents, and GitHub is like having that filing system backed up to the cloud where others can collaborate with you.</p>
<h2>Installing Git (first things first)</h2>
<p>Before we can configure Git, we need to make sure it's actually installed on your system. Let's check if you already have it:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> <span class="token parameter variable">--version</span></code></pre>
<p>If you see something like <code>git version 2.39.0</code>, you're good to go! If you get a "command not found" error, you need to install Git.</p>
<h3>On macOS</h3>
<p>The easiest way is using Homebrew (if you have it):</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">brew <span class="token function">install</span> <span class="token function">git</span></code></pre>
<p>Or download the installer from <a rel="noreferrer" target="_blank" href="https://git-scm.com/download/mac">git-scm.com</a>. macOS also comes with an older version of Git that gets installed when you run <code>git</code> for the first time, but I recommend getting the latest version.</p>
<h3>On Windows</h3>
<p>Download the installer from <a rel="noreferrer" target="_blank" href="https://git-scm.com/download/win">git-scm.com</a>. It comes with Git Bash, which gives you a Unix-like terminal that's perfect for running Git commands.</p>
<h3>On Linux</h3>
<p>Most Linux distributions include Git in their package managers:</p>
<p><strong>Ubuntu/Debian:</strong></p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> update
<span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token function">git</span></code></pre>
<p><strong>CentOS/RHEL/Fedora:</strong></p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">sudo</span> yum <span class="token function">install</span> <span class="token function">git</span>
<span class="token comment"># or on newer versions:</span>
<span class="token function">sudo</span> dnf <span class="token function">install</span> <span class="token function">git</span></code></pre>
<p>Once installed, run <code>git --version</code> again to confirm it's working.</p>
<h2>Setting up Git (the boring but necessary part)</h2>
<p>Before you start, you need to tell Git who you are. Run these commands once:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> config <span class="token parameter variable">--global</span> user.name <span class="token string">"Your Name"</span>
<span class="token function">git</span> config <span class="token parameter variable">--global</span> user.email <span class="token string">"[email protected]"</span></code></pre>
<p>That's it. Now Git will stamp your name on every commit you make. You only need to do this once per computer.</p>
<h2>The basic Git workflow that changed my coding life</h2>
<p>Here's the workflow I use every single day:</p>
<h3>1. Check what's changed</h3>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> status</code></pre>
<p>This shows you which files have been modified, added, or deleted. I probably run this command 50 times a day. It's like asking Git "what's different since my last commit?"</p>
<h3>2. Stage your changes</h3>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> <span class="token function">add</span> <span class="token builtin class-name">.</span></code></pre>
<p>This tells Git "I want to include all these changes in my next commit." You can also add specific files with <code>git add filename.js</code> if you want to be more selective.</p>
<h3>3. Commit your changes</h3>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> commit <span class="token parameter variable">-m</span> <span class="token string">"Add user authentication system"</span></code></pre>
<p>This creates a permanent snapshot of your staged changes. Write a clear message describing what you did. Future you will thank you for this.</p>
<h3>4. Push to GitHub</h3>
<p>Before you can push, you need to connect your local repository to a GitHub repository. Here's how:</p>
<p><strong>First time setup:</strong></p>
<ol>
<li>Create a new repository on GitHub (go to github.com, click the "+" icon, select "New repository")</li>
<li>Copy the repository URL (it looks like <code>https://github.com/yourusername/your-repo-name.git</code>)</li>
<li>Connect your local repo to GitHub:</li>
</ol>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> remote <span class="token function">add</span> origin https://github.com/yourusername/your-repo-name.git
<span class="token function">git</span> branch <span class="token parameter variable">-M</span> main
<span class="token function">git</span> push <span class="token parameter variable">-u</span> origin main</code></pre>
<p><strong>After the first setup:</strong></p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> push</code></pre>
<p>The first command (<code>git remote add origin</code>) tells Git where your GitHub repository is located. The second command (<code>git branch -M main</code>) ensures your main branch is named "main" (GitHub's default). The third command (<code>git push -u origin main</code>) pushes your code and sets up tracking so future pushes just need <code>git push</code>.</p>
<p>This uploads your commits to GitHub, backing up your work and making it available to collaborators.</p>
<p>I do this cycle constantly – sometimes every 10-15 minutes when I'm actively coding with AI assistance. Each commit is a checkpoint I can return to if things go wrong.</p>
<h2>Creating a repository (your project's home)</h2>
<p>To start tracking a new project:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> init</code></pre>
<p>This creates a hidden <code>.git</code> folder in your current directory and starts tracking changes.</p>
<p>If you want to work on an existing project from GitHub:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> clone https://github.com/username/repository-name.git</code></pre>
<p>This downloads the entire project and its history to your computer. For example, look how I am cloning a GitHub repo called <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/optisharp">optisharp</a> to my computer.</p>
<p><img alt="Cloning a repo from GitHub" decoding="async" src="https://assets.deepakness.com/blog/git-for-vibe-coders/git-clone-command.png"></p>
<h2>Branches: Your experimentation playground</h2>
<p>Branches are where Git really shines for vibe coders. Think of them <a rel="noreferrer" target="_blank" href="https://deepakness.com/raw/git-branching/">as parallel timelines</a> where you can try things without affecting your main code.</p>
<p>Create a new branch:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> checkout <span class="token parameter variable">-b</span> experiment/new-feature</code></pre>
<p>Now you can ask AI to implement that crazy idea you had. If it works great, you can merge it back. If it breaks everything, just switch back to your main branch:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> checkout main</code></pre>
<p>And delete the failed experiment:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> branch <span class="token parameter variable">-d</span> experiment/new-feature</code></pre>
<p>I use branches for literally everything:</p>
<ul>
<li><code>fix/header-bug</code> for bug fixes</li>
<li><code>feature/payment-integration</code> for new features</li>
<li><code>experiment/ai-suggestions</code> for testing AI-generated code</li>
</ul>
<h2>The .gitignore file (don't commit everything)</h2>
<p>Some files shouldn't be tracked by Git. Create a <code>.gitignore</code> file in your project root:</p>
<pre><code>node_modules/
.env
*.log
dist/
.DS_Store
</code></pre>
<p>This tells Git to ignore:</p>
<ul>
<li><code>node_modules/</code> (huge folder of dependencies)</li>
<li><code>.env</code> (contains secret keys)</li>
<li><code>*.log</code> (log files)</li>
<li><code>dist/</code> (build output)</li>
<li><code>.DS_Store</code> (macOS system files)</li>
</ul>
<p>Never commit secrets, large binary files, or generated content. Your repository should only contain source code and configuration files.</p>
<h2>My vibe coding safety rules</h2>
<p>After months of AI-assisted coding, here are the habits that have saved me countless times:</p>
<h3>Commit before asking AI to change anything major</h3>
<p>Before telling Claude "refactor this entire component," commit your current working state. This gives you an instant undo button.</p>
<h3>Use descriptive commit messages</h3>
<p>Instead of "updates" or "fixes," write:</p>
<ul>
<li>"Add user registration form with validation"</li>
<li>"Fix mobile navigation menu bug"</li>
<li>"Refactor database connection logic"</li>
</ul>
<h3>Review AI changes before committing</h3>
<p>Use <code>git diff</code> to see exactly what changed:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> <span class="token function">diff</span></code></pre>
<p>This shows you line-by-line what the AI added, removed, or modified. Sometimes AI changes more than you expected.</p>
<h3>Push frequently</h3>
<p>Don't let a day go by without pushing to GitHub. Your laptop could die, you could spill coffee on it, or you might accidentally run <code>rm -rf</code> in the wrong directory (yes, I've done this).</p>
<h2>GitHub: Your cloud backup and collaboration hub</h2>
<p>GitHub adds powerful features on top of Git:</p>
<p><strong>Issues</strong>: Track bugs and feature requests
<strong>Pull Requests</strong>: Review code changes before merging<br>
<strong>Actions</strong>: Automate testing and deployment
<strong>Releases</strong>: Tag stable versions of your code</p>
<p>Even as a solo vibe coder, I use GitHub Issues to track what I want to build next. It's like having a todo list that's tied to your codebase.</p>
<h2>When things go wrong (and they will)</h2>
<h3>Undoing your last commit</h3>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> reset <span class="token parameter variable">--soft</span> HEAD~1</code></pre>
<p>This undoes your last commit but keeps the changes in your working directory. Useful when you commit too early or have a typo in your commit message.</p>
<h3>Going back to a previous commit</h3>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> log <span class="token parameter variable">--oneline</span></code></pre>
<p>This shows your commit history. Find the commit you want to return to and:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">git</span> checkout abc1234</code></pre>
<p>(Replace <code>abc1234</code> with the actual commit hash)</p>
<h3>Dealing with merge conflicts</h3>
<p>Sometimes Git can't automatically merge changes. When this happens, Git will mark the conflicting sections in your files:</p>
<pre><code><<<<<<< HEAD
Your current code
=======
The conflicting code
>>>>>>> branch-name
</code></pre>
<p>Edit the file to keep what you want, remove the conflict markers, then commit the resolved version.</p>
<h2>My current vibe coding setup</h2>
<p>Here's how I actually use Git and GitHub in my day-to-day AI-assisted coding:</p>
<ol>
<li><strong>Start with a branch</strong> for each new feature or experiment</li>
<li><strong>Commit every 15-20 minutes</strong> when actively coding</li>
<li><strong>Push at least once per coding session</strong> for backup</li>
<li><strong>Use clear commit messages</strong> that explain the "why," not just the "what"</li>
<li><strong>Review AI changes</strong> with <code>git diff</code> before committing</li>
<li><strong>Create Pull Requests</strong> even for solo projects to review my own work</li>
</ol>
<p>This might seem like overkill, but it's saved me so many times when AI-generated code broke something unexpectedly.</p>
<h2>The honest truth about learning Git</h2>
<p>Git has a learning curve. Commands like <code>rebase</code>, <code>cherry-pick</code>, and <code>bisect</code> can seem intimidating at first. But here's the thing: you don't need to master everything on day one.</p>
<p>Start with the basics I've covered here:</p>
<ul>
<li><code>git add</code></li>
<li><code>git commit</code></li>
<li><code>git push</code></li>
<li><code>git status</code></li>
<li><code>git checkout</code> (for branches)</li>
</ul>
<p>These five commands will handle 90% of your needs as a vibe coder. You can learn the advanced stuff when you actually need it.</p>
<p>The key is to start using Git now, even if you don't understand everything. It's better to have imperfect version control than no version control at all.</p>
<h2>Why this matters for vibe coders specifically</h2>
<p>Traditional programmers write code line by line, understanding every character. They might go hours between commits because they're building something incrementally.</p>
<p>Vibe coders work differently. We prompt AI, get large chunks of generated code, test it, and iterate. This creates unique challenges:</p>
<ul>
<li><strong>Large, sudden changes</strong> that might break existing functionality</li>
<li><strong>Code we don't fully understand</strong> initially</li>
<li><strong>Rapid experimentation</strong> with different approaches</li>
<li><strong>Need to roll back</strong> entire features quickly</li>
</ul>
<p>Git is perfectly suited for this workflow. Every commit is a checkpoint, every branch is an experiment, and every push is a backup of your progress.</p>
<h2>Getting started today</h2>
<p>If you're not using Git yet, here's what to do right now:</p>
<ol>
<li>Install Git from <a rel="noreferrer" target="_blank" href="https://git-scm.com/">git-scm.com</a></li>
<li>Create a GitHub account at <a rel="noreferrer" target="_blank" href="https://github.com/">github.com</a></li>
<li>Run the config commands to set your name and email</li>
<li>Go to your current project and run <code>git init</code></li>
<li>Create a <code>.gitignore</code> file for your project type</li>
<li>Add and commit your existing code</li>
<li>Create a repository on GitHub and push your code</li>
</ol>
<p>Don't wait until you "understand Git better." Start with the basics and learn as you go.</p>
<h2>Final thoughts</h2>
<p>Git and GitHub aren't just tools for professional developers – they're essential safety equipment for anyone doing vibe coding. The ability to experiment fearlessly, knowing you can always undo changes, is liberating.</p>
<p>I've seen too many people lose hours of work because they didn't have version control. Don't be one of them. Start using Git today, commit frequently, and let AI help you build amazing things without the fear of breaking everything.</p>
<p>Your future self will thank you when you can confidently tell Claude Code or Cursor to "refactor this entire codebase" knowing you can always go back to the last working version.</p>
<p>Happy vibe coding!</p>
How to Run Omarchy on a MacBook via UTM2025-09-02T16:20:24Zhttps://deepakness.com/blog/omarchy-on-mac-via-utm/A quick guide to running Omarchy ISO on a MacBook M2 Air for testing via UTM.<p>During my college days, I used Manjaro Linux for 4-5 years and I loved it. And I am feeling the same excitement again after learning about <a rel="noreferrer" target="_blank" href="https://omarchy.org/">DHH's new Arch-based Omarchy Linux distro</a>.</p>
<p>Currently, I use a MacBook M2 Air and do not have another machine to install and use the distro, but I wanted to give it a try. So I tried running Omarchy via <a rel="noreferrer" target="_blank" href="https://mac.getutm.app/">UTM (VM for macOS)</a> on my machine, and I finally got it working after almost an hour of trying different settings in UTM. And here's how it looked:</p>
<p><img alt="Omarchy installed on MacBook via UTM" decoding="async" src="https://assets.deepakness.com/blog/omarchy-on-mac-via-utm/omarchy-live.png"></p>
<blockquote>
<p>But please note that I wasn't able to actually work on it, as it was considerably slow and laggy. So... install it only if you just want to play with it.</p>
</blockquote>
<p>Here's how I got it working...</p>
<h2>1. Download Omarchy ISO and install UTM</h2>
<p>The very first step is to download the Omarchy ISO from <a rel="noreferrer" target="_blank" href="https://omarchy.org/">their official website</a>, and save somewhere on your computer. It was around 1.5 GB in size when I tried, but might change a bit in the future.</p>
<p>Then you have to install UTM by downloading the <code>.dmg</code> from <a rel="noreferrer" target="_blank" href="https://mac.getutm.app/">their official website</a>, and installing them by double-clicking on the file, just like how you install any other macOS app.</p>
<h2>2. Set up a new virtual machine</h2>
<p>Open the UTM app and set up a new virtual machine by clicking on the <strong>Create a New Virtual Machine</strong> button, and below are the options you have to change – leave options, which I haven't mentioned here, as default.</p>
<ol>
<li>Select <strong>Emulate</strong> option, as we have to run another CPU architecture</li>
<li>Select <strong>Linux</strong> option</li>
<li>Click on the <strong>Browse</strong> button and select the Omarchy ISO file, and click <strong>Continue</strong></li>
<li>Select <strong>x86_64</strong> in the Architecture dropdown, leave System as the default</li>
<li>Make the Memory to be 8 GB i.e. 8192 MiB, and click <strong>Continue</strong></li>
<li>Either leave the size to be 64 GiB or can also make it 32 GiB, click <strong>Continue</strong></li>
<li>Optional, but you can select a shared directory path, click <strong>Continue</strong></li>
<li>Give your VM a name and <strong>Save</strong></li>
<li>Right-click on the VM in UTM sidebar, and select <strong>Edit</strong> option</li>
<li>Go to the Display section</li>
<li>Select <code>virtio-gpu-pci</code> in the Emulated Display Card dropdown (see screenshot below)</li>
<li>Check the Retina Mode option and <strong>Save</strong></li>
</ol>
<p>Below I am adding some screenshots, in case that's helpful for you during the process.</p>
<p><img alt="Start Omarchy installation process" decoding="async" src="https://assets.deepakness.com/blog/omarchy-on-mac-via-utm/utm-omarchy.png"></p>
<p><img alt="Selecting the correct Display option" decoding="async" src="https://assets.deepakness.com/blog/omarchy-on-mac-via-utm/utm-display.png"></p>
<p>Let's move to the next and last step now.</p>
<h2>3. Start the installation process</h2>
<p>Click on the big <strong>Play button</strong> (see above screenshot), and it will start the installation process. It will ask you some details during the installation process which you can learn more about in the <a rel="noreferrer" target="_blank" href="https://learn.omacom.io/2/the-omarchy-manual">Omarchy documentation</a>.</p>
<p>Now it will take time... a lot of time, actually.</p>
<p>In my case, it took me more than 30 minutes. You have to just let it work in the background and do nothing, maybe grab a cup of coffee or can even take a quick walk outside.</p>
<p>After the installation, it automatically restarts and might again ask you for the details that you just provided during the installation.</p>
<p>But <strong>do not</strong> move forward.</p>
<p>Instead, shut down the VM, right-click on the VM in the sidebar and select the <strong>Edit</strong> option. Go to the first IDE option shown in the sidebar below, and click on the Clear button beside Browse to remove the selected Omarchy ISO (see below screenshot for more clarity).</p>
<p><img alt="Clear Omarchy ISO" decoding="async" src="https://assets.deepakness.com/blog/omarchy-on-mac-via-utm/clear-omarchy-iso.png"></p>
<p>Now <strong>Save</strong>, and restart the VM by clicking on the <strong>Play</strong> button. You should be asked to enter your password and see your beautiful Omarchy desktop.</p>
<p>And you're done!</p>
<p>As I have mentioned in the start, this will not be very performant, but you can definitely get the glimpse of Omarchy.</p>
Are Double Quotes Replacing Single Quotes?2025-07-20T01:16:59Zhttps://deepakness.com/blog/double-vs-single-quotes/How quotation mark usage is changing across regions, platforms, and generations.<p>Recently, I was editing a document and noticed that I instinctively used double quotation marks (" ") instead of single ones (' '). It made me pause for a second – didn't I grow up using single quotes for things like speech and dialogue?</p>
<p>That led me down a rabbit hole of how quotation mark usage has been changing. And yeah, things have shifted a bit – but it's not as straightforward as "double quotes are taking over single quotes."</p>
<h2>It depends on where you're from</h2>
<p>In the US, double quotes have always been the default. In the UK, it's traditionally been single quotes. But this line is starting to blur. Some big UK newspapers (like <em>The Telegraph</em> and <em>The Times</em>) now follow what's more common in the US. Even the term "quotation marks" is <a rel="noreferrer" target="_blank" href="https://stancarey.wordpress.com/2019/05/31/quotation-marks-or-inverted-commas/">becoming more common</a> than the older "inverted commas" in Britain.</p>
<p><strong>Fun fact</strong>: double quotes were actually more popular in the UK before the 1950s. Single quotes came in later, mainly because they took up less space in print.</p>
<p><img alt="Screenshot of a Telegraph article showing use of double quotes" decoding="async" src="https://assets.deepakness.com/blog/double-vs-single-quotes/telegraph-uk.png"></p>
<p>But look at <a rel="noreferrer" target="_blank" href="https://www.telegraph.co.uk/news/2025/07/19/influencer-who-went-viral-at-wimbledon-is-actually-ai/">this today's article</a> in the Telegraph UK, they haven't used any single quote in the whole of the article, but only double quotes. In fact, none of their articles have single quotes anymore.</p>
<h2>But... publishing still has its own rules</h2>
<p>Publishing houses and editors haven't all switched to one style. The <a rel="noreferrer" target="_blank" href="https://en.wikipedia.org/wiki/AP_Stylebook">AP Stylebook</a> still <a rel="noreferrer" target="_blank" href="https://www.ragan.com/ap-style-quotation-marks/">prefers double quotes</a> in the US. Meanwhile, British publishers stick to their traditional style – unless the audience is more international. In that case, they might lean American.</p>
<p><img alt="Screenshot from the AP Stylebook showing quotation mark rules" decoding="async" src="https://assets.deepakness.com/blog/double-vs-single-quotes/ap-style-double-quotes.png"></p>
<p>Also, with things going more digital, there's pressure to make everything consistent. For example, many digital platforms don't even treat curly quotes properly in search – which is wild if you care about accuracy in research.</p>
<h2>Then there's texting, typing, and social media</h2>
<p>This is where things get messy. Most of <a rel="noreferrer" target="_blank" href="https://washfm.iheart.com/featured/toby-and-chilli-mornings/content/2023-09-13-younger-generations-do-not-really-care-about-grammar/">Gen Z don't bother with proper punctuation</a> when texting. And I totally get it – it's just faster. Pulling up smart quotes on a mobile keyboard is annoying.</p>
<p><img alt="Usage of quotation while texting" decoding="async" src="https://assets.deepakness.com/blog/double-vs-single-quotes/quotation-in-texting.png"></p>
<p>On social media, people have created their own ways to show quotes. Sometimes they don't even use punctuation. They just use formatting, screenshots, or emojis to make a point.</p>
<p>So if you're typing on your phone, you're probably using straight quotes, or skipping them altogether. I do it too, especially in casual messages or tweets.</p>
<h2>Style guides are slowly adapting</h2>
<p>Style guides from places like Notre Dame and newer journalist groups have started updating their rules. They know people aren't typing the same way they did 10 years ago. So while they still <a rel="noreferrer" target="_blank" href="https://ndworks.nd.edu/news/editors-note-single-or-double-quotes/">prefer double quotes in formal writing</a>, they're acknowledging that habits are changing – especially online.</p>
<h2>The real picture</h2>
<p>If you're wondering whether single quotes are disappearing, the answer is: <strong>yes, somewhat</strong>.</p>
<p>But they're also being used differently depending on the context, and both single and double quotes are still around. It's just that we're now using them based on what feels right for the situation – and honestly, that makes sense.</p>
Calling Myself an Internet Generalist2025-06-09T16:44:04Zhttps://deepakness.com/blog/internet-generalist/Starting to call myself an internet generalist from today.<p>Starting today, I'm calling myself an internet generalist – because it describes me well.</p>
<p>If you're wondering what that means:</p>
<p>An <strong>internet generalist</strong> is someone with a broad range of skills and knowledge related to working online. They might be familiar with things like:</p>
<ul>
<li>Programming</li>
<li>Writing and editing</li>
<li>Online marketing</li>
<li>Graphics design</li>
<li>Leveraging AI, etc.</li>
</ul>
<p>Basically, they’re good at handling many kinds of online tasks and are especially useful in small teams or startups, where people often need to wear multiple hats.</p>
Trying the Sarvam-M 24B Indic LLM2025-05-27T03:41:59Zhttps://deepakness.com/blog/sarvam-m/Sarvam AI has launched its very first hybrid Indic LLM called Sarvam-M, and I'm trying to see how it performs.<p>Sarvam AI has released its very first <a rel="noreferrer" target="_blank" href="https://www.sarvam.ai/blogs/sarvam-m">Hybrid Indic LLM called Sarvam-M 24b</a> built on top of the Mistral-Small model (hence the <code>M</code> in the name). It's a text-only model trained on all major Indian languages. You can run it locally on your computer <a rel="noreferrer" target="_blank" href="https://huggingface.co/sarvamai/sarvam-m">as it's open-source</a>, use <a rel="noreferrer" target="_blank" href="https://dashboard.sarvam.ai/playground">on the playground</a>, and also <a rel="noreferrer" target="_blank" href="https://docs.sarvam.ai/sarvam-api-docs">available via API</a>.</p>
<p>I came to know about the launch from X and there are mixed opinions about the launch. But I am going to try it anyway and document my learnings in the post.</p>
<h2>The first impression</h2>
<p>I skimmed through <a rel="noreferrer" target="_blank" href="https://www.sarvam.ai/blogs/sarvam-m">the launch blog post</a> to see the benchmark and the numbers are impressive when compared against Mistral Small 24B, Gemma 3 27B, Llama 4 Scout 17B/109B, and Llama 3.3 70B. It's claimed to perform:</p>
<ul>
<li>20% better on Indian language tasks</li>
<li>21.6% better on math tasks</li>
<li>17.6% better on programming tasks</li>
</ul>
<p>The model is trained to understand and generate text in 10 Indian languages, including Hindi, Tamil, Telugu, Malayalam, Punjabi, Odia, Gujarati, Marathi, Kannada, and Bengali.</p>
<p>As mentioned in the post, they used Supervised Fine-Tuning (SFT), Reinforcement Learning with Verifiable Rewards (RLVR), and Inference Optimization to train the model.</p>
<h2>Trying the Sarvam Playground</h2>
<p>I <a rel="noreferrer" target="_blank" href="https://dashboard.sarvam.ai/playground">visited this URL</a> to access the Playground and signed up using my Google account, and landed on playground page that looked like this.</p>
<p><img alt="Sarvam AI Playground" decoding="async" src="https://assets.deepakness.com/blog/sarvam-m/sarvam-ai-playground.png"></p>
<p>I asked it to talk to me in the Hindi language and it immediately replied to me in the language. And it was very fast, definitely faster than ChatGPT and Gemini. If you'd like, below is the exact response that it gave.</p>
<p><img alt="Asking Sarvam AI to talk in Hindi" decoding="async" src="https://assets.deepakness.com/blog/sarvam-m/asking-sarvam-m-to-talk-in-hindi.png"></p>
<p>But what impressed me the most is its "Text to Speech" tool. When I clicked on the option, it preloaded a line of text in the Odia language and perfectly spoke the language when I clicked on the generate button.</p>
<p>To test further, I gave it the below paragraph in English, asked it to translate to Odia, and then converted it to <a rel="noreferrer" target="_blank" href="https://deepakness.com/audio/sarvam-m.wav">an audio</a>. I'm putting everything below.</p>
<p><strong>The English text:</strong></p>
<pre><code>The solar system is a group of objects that move around the Sun. The Sun is at the center and is the biggest object in the system. It gives off heat and light that make life on Earth possible. There are eight main planets that go around the Sun: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. These planets move in paths called orbits.
</code></pre>
<p><strong>Translated Odia text:</strong></p>
<pre><code>ସୌରଜଗତ ହେଉଛି ସୂର୍ଯ୍ୟଙ୍କ ଚାରିପାଖେ ଘୂର୍ଣ୍ଣନ କରୁଥିବା ବସ୍ତୁମାନଙ୍କର ଏକ ସମୂହ। ସୂର୍ଯ୍ୟ କେନ୍ଦ୍ରରେ ଅବସ୍ଥିତ ଏବଂ ସୌରଜଗତର ସବୁଠାରୁ ବଡ଼ ବସ୍ତୁ। ଏହା ଉତ୍ତାପ ଏବଂ ଆଲୋକ ବିସ୍ତାର କରେ ଯାହା ପୃଥିବୀରେ ଜୀବନ ସମ୍ଭବ କରିଥାଏ। ସୂର୍ଯ୍ୟଙ୍କ ଚାରିପାଖେ ପରିକ୍ରମା କରୁଥିବା 8ଟି ମୁଖ୍ୟ ଗ୍ରହ ଅଛନ୍ତି: ବୁଧ, ଶୁକ୍ର, ପୃଥିବୀ, ମଙ୍ଗଳ, ବୃହସ୍ପତି, ଶନି, ଅରୁଣ, ଏବଂ ନେପ୍ଚ୍ୟୁନ। ଏହି ଗ୍ରହମାନେ କକ୍ଷପଥ ନାମକ ପଥରେ ଘୂର୍ଣ୍ଣନ କରନ୍ତି।
</code></pre>
<p><strong>And the final audio:</strong></p>
<audio controls>
<source src="https://deepakness.com/audio/sarvam-m.wav" type="audio/wav">
Your browser does not support the audio element.
</audio>
<p>I don't read the Odia text, but I know how to speak, and can say that the audio quality is very good. It speaks perfectly as they speak on the news channels.</p>
<p>I'm very impressed with the output.</p>
<p>Apart from this, they have other tools like Speech to Text, Speech Translate, Transliterate, Translate, and Language Identification. I tried the Speech to Text tool by uploading the same audio in Odia language, and it was able to transcribe perfectly transcribe it.</p>
<p><img alt="Sarvam AI from Odia Speech to Text" decoding="async" src="https://assets.deepakness.com/blog/sarvam-m/speech-to-text.png"></p>
<p>Similarly, other tools are good as well.</p>
<blockquote>
<p>One problem though, when I put 2 paragraphs of text in the text-to-speech tool, it doesn't work and just says "internal server error". The error should be more descriptive.</p>
</blockquote>
<h2>Trying via the API</h2>
<p>I received ₹1,000 worth of free credits when I created a new account on the platform, so I also decided to try it via the API and see how it performs.</p>
<p>I went through the docs and created a simple interface to try the API using HTML and JavaScript, everything is hard-coded as you can see here:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token doctype"><span class="token punctuation"><!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>title</span><span class="token punctuation">></span></span>Sarvam Chat<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>title</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h1</span><span class="token punctuation">></span></span>Ask Sarvam<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h1</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>text<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>userInput<span class="token punctuation">"</span></span> <span class="token attr-name">placeholder</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Type your question...<span class="token punctuation">"</span></span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">width</span><span class="token punctuation">:</span> 300px<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token special-attr"><span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value javascript language-javascript"><span class="token function">callSarvam</span><span class="token punctuation">(</span><span class="token punctuation">)</span></span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>Send<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h2</span><span class="token punctuation">></span></span>Response:<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h2</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>pre</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>output<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Waiting for input...<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>pre</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">callSarvam</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> userMessage <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'userInput'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>value<span class="token punctuation">;</span>
<span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> <span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://api.sarvam.ai/v1/chat/completions'</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">method</span><span class="token operator">:</span> <span class="token string">'POST'</span><span class="token punctuation">,</span>
<span class="token literal-property property">headers</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token string-property property">'Authorization'</span><span class="token operator">:</span> <span class="token string">'Bearer YOUR_API_KEY_HERE'</span><span class="token punctuation">,</span>
<span class="token string-property property">'Content-Type'</span><span class="token operator">:</span> <span class="token string">'application/json'</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token literal-property property">body</span><span class="token operator">:</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">messages</span><span class="token operator">:</span> <span class="token punctuation">[</span>
<span class="token punctuation">{</span> <span class="token literal-property property">role</span><span class="token operator">:</span> <span class="token string">'system'</span><span class="token punctuation">,</span> <span class="token literal-property property">content</span><span class="token operator">:</span> <span class="token string">"you're sarvam, an llm"</span> <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span> <span class="token literal-property property">role</span><span class="token operator">:</span> <span class="token string">'user'</span><span class="token punctuation">,</span> <span class="token literal-property property">content</span><span class="token operator">:</span> userMessage <span class="token punctuation">}</span>
<span class="token punctuation">]</span><span class="token punctuation">,</span>
<span class="token literal-property property">model</span><span class="token operator">:</span> <span class="token string">'sarvam-m'</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>response<span class="token punctuation">.</span>ok<span class="token punctuation">)</span> <span class="token punctuation">{</span>
document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'output'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent <span class="token operator">=</span> <span class="token string">'Error: '</span> <span class="token operator">+</span> response<span class="token punctuation">.</span>status<span class="token punctuation">;</span>
<span class="token keyword">return</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token keyword">await</span> response<span class="token punctuation">.</span><span class="token function">json</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> content <span class="token operator">=</span> data<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>message<span class="token punctuation">.</span>content<span class="token punctuation">;</span>
document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'output'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>textContent <span class="token operator">=</span> content<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span></code></pre>
<p>You just need to replace <code>YOUR_API_KEY_HERE</code> with your actual API key, and then you can ask questions and get answers on the page. I tried asking a few questions and it did answer as it does on the playground, the text response from the API is fast as well.</p>
<h3>Sarvam API pricing</h3>
<p>Below is the <a rel="noreferrer" target="_blank" href="https://dashboard.sarvam.ai/billing/pricing">pricing</a> for different models in Indian Rupees (₹) as present on the pricing page.</p>
<table>
<thead>
<tr>
<th><strong>Category</strong></th>
<th><strong>Service</strong></th>
<th><strong>Price</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Speech APIs</strong></td>
<td>Speech to Text</td>
<td>₹30 / hour</td>
</tr>
<tr>
<td></td>
<td>Speech to Text & Translate</td>
<td>₹30 / hour</td>
</tr>
<tr>
<td></td>
<td>Speech to Text with Diarization</td>
<td>₹45 / hour</td>
</tr>
<tr>
<td></td>
<td>Speech to Text, Translate & Diarization</td>
<td>₹45 / hour</td>
</tr>
<tr>
<td><strong>Language Tools</strong></td>
<td>Translate</td>
<td>₹20 / 10K chars</td>
</tr>
<tr>
<td></td>
<td>Transliterate</td>
<td>₹20 / 10K chars</td>
</tr>
<tr>
<td></td>
<td>Language Identification</td>
<td>₹3.5 / 10K chars</td>
</tr>
<tr>
<td><strong>Text to Speech</strong></td>
<td>Text to Speech</td>
<td>₹15 / 10K chars</td>
</tr>
<tr>
<td><strong>Analytics</strong></td>
<td>Call Analytics</td>
<td>₹112 / hour</td>
</tr>
</tbody>
</table>
<p>I don't know why they haven't mentioned the pricing for chat completions anywhere on the pricing page, or on any page.</p>
<h2>But... do people care about an Indic LLM?</h2>
<p>Yes, the question is, do people of India actually need an Indic model like this? Several people like <a rel="noreferrer" target="_blank" href="https://x.com/deedydas/status/1926089907988046261">Deedy</a> and <a rel="noreferrer" target="_blank" href="https://x.com/arpit_bhayani/status/1926104122735890910">Arpit Bhayani</a> mention that there's not enough demand for a model like this and that's why there are only a few hundred downloads of the model on Hugging Face.</p>
<p>I think, main reasons for less number of downloads are:</p>
<ol>
<li>very fewer people care about LLMs in India as of now</li>
<li>most people are end users and won't be downloading the model from Hugging Face, and</li>
<li>this is not marketed well enough, people need to be made aware of what this can do</li>
</ol>
<p>Honestly, I loved the model. If I have to make a multilingual website then I would prefer Sarvam-M to translate my content over OpenAI's or Gemini's models without any second thoughts.</p>
<p><strong>🗓️ Update: June 4, 2025</strong></p>
<p>The number of downloads on <a rel="noreferrer" target="_blank" href="https://huggingface.co/sarvamai/sarvam-m">Hugging Face</a> has increased to 269,186 at the time to writing this post. But I am not seeing enough buzz about Sarvam AI on social media and also on YouTube, so it's hard to say if the number is genuine or manipulated.</p>
<p><strong>Overall, I liked the below things:</strong></p>
<ul>
<li>ability to generate output in Indian languages</li>
<li>formatting and quality of the output</li>
<li>inference speed when asking questions</li>
</ul>
<p><strong>And a few things that can be improved:</strong></p>
<ul>
<li>documentation for different endpoints</li>
<li>making people aware of the model</li>
</ul>
<p>I also <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=Fs51ydxH6XY">recorded a video of myself</a> trying the new Sarvam-M model via the playground and API, and have put my thoughts into it.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="Fs51ydxH6XY" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="Fs51ydxH6XY" style="background-image: url('https://i.ytimg.com/vi/Fs51ydxH6XY/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>That's it.</p>
<p>I am still using and testing the model and will keep updating this page if I learn something new.</p>
Trying the Qwen3-8B Model Locally2025-05-04T02:42:52Zhttps://deepakness.com/blog/trying-qwen3-8b/Trying the qwen3-8b model locally and sharing my learnings.<p>I have been hearing a lot of good things about the <code>qwen3-8b</code> model lately, in fact, I also read about it in a <a rel="noreferrer" target="_blank" href="https://simonwillison.net/2025/May/2/qwen3-8b/">post by Simon</a>.</p>
<p>So, I had to try it and I will be sharing my learnings in this post.</p>
<h2>Installation</h2>
<p>I use Ollama so the installation process was easy, I just ran the following terminal command and the model got installed within a minute. You can visit the <a rel="noreferrer" target="_blank" href="https://ollama.com/library/qwen3:8b">Ollama page for qwen3 here</a>.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">ollama run qwen3:8b</code></pre>
<p>For the <code>8b</code> model, it downloaded a 5.2GB file.</p>
<h2>Initial testing</h2>
<p>At first, I just sent "hi" and then only I got to know that it's a thinking model as its thoughts were being shown between <code><think></code> <code></think></code> tags. And then it replied with "Hello! 😊 How can I assist you today?" as you can see in the below screenshot.</p>
<p><img alt="Running Qwen3-8B model via Ollama" decoding="async" src="https://assets.deepakness.com/blog/trying-qwen3-8b/qwen3-8b-via-ollama.png"></p>
<p>I am using a MacBook M2 Air 16GB device and the evaluation rate was 16.34 tokens per second, as you can see in the above screenshot.</p>
<h2>Coding test</h2>
<p>I asked it to generate a simple tic-tac-toe game by giving it the following prompt:</p>
<pre class="language-plaintext" tabindex="0"><code class="language-plaintext">create a tic tac toe game in python</code></pre>
<p>And, to my surprise, it created the best tic-tac-toe game I have ever played in the terminal and it ran without any errors. If you want to take a look, below is the Python code that it gave me:</p>
<pre class="language-python" tabindex="0"><code class="language-python"><span class="token keyword">def</span> <span class="token function">check_winner</span><span class="token punctuation">(</span>board<span class="token punctuation">,</span> player<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token comment"># Check rows</span>
<span class="token keyword">for</span> row <span class="token keyword">in</span> board<span class="token punctuation">:</span>
<span class="token keyword">if</span> row<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">==</span> row<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">==</span> row<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span> <span class="token operator">==</span> player<span class="token punctuation">:</span>
<span class="token keyword">return</span> <span class="token boolean">True</span>
<span class="token comment"># Check columns</span>
<span class="token keyword">for</span> col <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">if</span> board<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span>col<span class="token punctuation">]</span> <span class="token operator">==</span> board<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">[</span>col<span class="token punctuation">]</span> <span class="token operator">==</span> board<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">[</span>col<span class="token punctuation">]</span> <span class="token operator">==</span> player<span class="token punctuation">:</span>
<span class="token keyword">return</span> <span class="token boolean">True</span>
<span class="token comment"># Check diagonals</span>
<span class="token keyword">if</span> board<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">==</span> board<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">==</span> board<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span> <span class="token operator">==</span> player<span class="token punctuation">:</span>
<span class="token keyword">return</span> <span class="token boolean">True</span>
<span class="token keyword">if</span> board<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span> <span class="token operator">==</span> board<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">==</span> board<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">==</span> player<span class="token punctuation">:</span>
<span class="token keyword">return</span> <span class="token boolean">True</span>
<span class="token keyword">return</span> <span class="token boolean">False</span>
<span class="token keyword">def</span> <span class="token function">is_draw</span><span class="token punctuation">(</span>board<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">for</span> row <span class="token keyword">in</span> board<span class="token punctuation">:</span>
<span class="token keyword">if</span> <span class="token string">' '</span> <span class="token keyword">in</span> row<span class="token punctuation">:</span>
<span class="token keyword">return</span> <span class="token boolean">False</span>
<span class="token keyword">return</span> <span class="token boolean">True</span>
<span class="token keyword">def</span> <span class="token function">print_board</span><span class="token punctuation">(</span>board<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">for</span> row <span class="token keyword">in</span> board<span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">'|'</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>row<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
board <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">[</span><span class="token string">' '</span> <span class="token keyword">for</span> _ <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">]</span> <span class="token keyword">for</span> _ <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
current_player <span class="token operator">=</span> <span class="token string">'X'</span>
<span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span>
print_board<span class="token punctuation">(</span>board<span class="token punctuation">)</span>
<span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span>
<span class="token keyword">try</span><span class="token punctuation">:</span>
move <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span><span class="token builtin">input</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Player </span><span class="token interpolation"><span class="token punctuation">{</span>current_player<span class="token punctuation">}</span></span><span class="token string">, enter your move (1-9): "</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">if</span> <span class="token number">1</span> <span class="token operator"><=</span> move <span class="token operator"><=</span> <span class="token number">9</span><span class="token punctuation">:</span>
row <span class="token operator">=</span> <span class="token punctuation">(</span>move <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">//</span> <span class="token number">3</span>
col <span class="token operator">=</span> <span class="token punctuation">(</span>move <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">%</span> <span class="token number">3</span>
<span class="token keyword">if</span> board<span class="token punctuation">[</span>row<span class="token punctuation">]</span><span class="token punctuation">[</span>col<span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token string">' '</span><span class="token punctuation">:</span>
board<span class="token punctuation">[</span>row<span class="token punctuation">]</span><span class="token punctuation">[</span>col<span class="token punctuation">]</span> <span class="token operator">=</span> current_player
<span class="token keyword">break</span>
<span class="token keyword">else</span><span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Cell already occupied. Try again."</span><span class="token punctuation">)</span>
<span class="token keyword">else</span><span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Invalid input. Please enter a number between 1 and 9."</span><span class="token punctuation">)</span>
<span class="token keyword">except</span> ValueError<span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Invalid input. Please enter a number between 1 and 9."</span><span class="token punctuation">)</span>
<span class="token keyword">if</span> check_winner<span class="token punctuation">(</span>board<span class="token punctuation">,</span> current_player<span class="token punctuation">)</span><span class="token punctuation">:</span>
print_board<span class="token punctuation">(</span>board<span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Player </span><span class="token interpolation"><span class="token punctuation">{</span>current_player<span class="token punctuation">}</span></span><span class="token string"> wins!"</span></span><span class="token punctuation">)</span>
<span class="token keyword">break</span>
<span class="token keyword">if</span> is_draw<span class="token punctuation">(</span>board<span class="token punctuation">)</span><span class="token punctuation">:</span>
print_board<span class="token punctuation">(</span>board<span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"It's a draw!"</span><span class="token punctuation">)</span>
<span class="token keyword">break</span>
current_player <span class="token operator">=</span> <span class="token string">'O'</span> <span class="token keyword">if</span> current_player <span class="token operator">==</span> <span class="token string">'X'</span> <span class="token keyword">else</span> <span class="token string">'X'</span>
<span class="token keyword">if</span> __name__ <span class="token operator">==</span> <span class="token string">"__main__"</span><span class="token punctuation">:</span>
main<span class="token punctuation">(</span><span class="token punctuation">)</span></code></pre>
<p>It looks like this when you play the game - it perfectly handles the win, lose, and draw cases.</p>
<pre class="language-plaintext" tabindex="0"><code class="language-plaintext">| | | |
| | | |
| | | |
Player X, enter your move (1-9): 1
|X| | |
| | | |
| | | |
Player O, enter your move (1-9): 5
|X| | |
| |O| |
| | | |
Player X, enter your move (1-9): 3
|X| |X|
| |O| |
| | | |
Player O, enter your move (1-9): 7
|X| |X|
| |O| |
|O| | |
Player X, enter your move (1-9): 9
|X| |X|
| |O| |
|O| |X|
Player O, enter your move (1-9): 4
|X| |X|
|O|O| |
|O| |X|
Player X, enter your move (1-9): 6
|X| |X|
|O|O|X|
|O| |X|
Player O, enter your move (1-9): 8
|X| |X|
|O|O|X|
|O|O|X|
Player X, enter your move (1-9): 2
|X|X|X|
|O|O|X|
|O|O|X|
Player X wins!</code></pre>
<p>But the only issue was, it took approximately 12 minutes to give the output as it kept thinking and thinking. I am sure that it's due to my computer's power, and it would have been faster on a computer with higher configuration.</p>
<blockquote>
<p>If you want to take a look at the entire output, including the <code><think></code> tags, you can click here to <a rel="noreferrer" target="_blank" href="https://deepakness.com/docs/trying-qwen3-8b.txt">see this .txt file</a>.</p>
</blockquote>
<p>Apart from this, I asked multiple simple coding and tech related questions and I'd say that I am very satisfied with it. It performs surprisingly well for its size.</p>
Learning about sqlite3 WASM2025-04-24T01:22:40Zhttps://deepakness.com/blog/sqlite3-wasm/Recently discovered that sqlite3 WebAssembly can run directly in the browser, so learning more about it.<p>I love sqlite3 and have used it for a few simple projects, but I recently came to know that <a rel="noreferrer" target="_blank" href="https://www.notion.com/blog/how-we-sped-up-notion-in-the-browser-with-wasm-sqlite">Notion uses sqlite3 WASM</a> in the browser for client-side caching – they have been able to improve their page navigation times by 20% by doing so.</p>
<p>I found it really interesting, so started reading more about it. Basically, this post is just my notes on the topic – I will be noting down whatever I read.</p>
<h2>What is sqlite3 WASM?</h2>
<p>sqlite3 WASM or WebAssembly is a version of the SQLite database that’s built to run in web browsers using WebAssembly. It lets you use a full SQLite database directly in the browser, powered by JavaScript and compiled C code, without needing a server.</p>
<p>And this version is <a rel="noreferrer" target="_blank" href="https://sqlite.org/wasm/doc/tip/about.md">officially supported</a> by the SQLite team.</p>
<h2>Features</h2>
<ol>
<li>Runs the complete SQLite engine within the browser using WebAssembly while supporting standard SQL queries and database operations without server-side components.</li>
<li>Supports multiple JavaScript APIs, like:
<ul>
<li>Low-level C API binding that provide direct access to SQLite's C API</li>
<li>High-level object-oriented API that offers a more JavaScript-friendly interface</li>
<li>Worker-based API that enables operations in web-workers to prevent blocking the main thread</li>
<li>Promise-based API that simplifies asynchronous operations with a Promise-based interface</li>
</ul>
</li>
<li>Utilizes the <a rel="noreferrer" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system">OPFS (Origin Private File System)</a> for persistent storage, so that the data remains available across sessions.</li>
<li>Allows the creation of virtual tables and table-valued functions directly in JavaScript and also supports custom builds with additional features or optimizations.</li>
</ol>
<h2>How Notion is using it</h2>
<p>Earlier, Notion was using IndexedDB for storing data in the browser but it had issues like storage limits and inconsistent performances across different browsers, so they switched to sqlite3 WASM solving all these issues.</p>
<ul>
<li>Notion uses OPFS to store data locally - persistent across sessions</li>
<li>To avoid slowing down, they run database operations in Web Workers in the background</li>
<li>Only the active tab handles all database writes, and other tabs send their request to this active tab</li>
</ul>
<p>Here's how they explain this:</p>
<blockquote>
<p>[...] only one tab is permitted to actually use its Web Worker. A SharedWorker is responsible for managing which is the “active tab.” When the active tab closes, the SharedWorker knows to select a new active tab.</p>
<p>To execute any SQLite query, the main thread of each tab sends that query to the SharedWorker, which redirects to the active tab’s dedicated Worker. Any number of tabs can make simultaneous SQLite queries as many times as they want, and it will always be routed to the single active tab.</p>
</blockquote>
<p>I also found a <a rel="noreferrer" target="_blank" href="https://github.com/rhashimoto/wa-sqlite/discussions/81">great discussion on the topic</a> on GitHub that you must check out.</p>
<p>I recommend reading the <a rel="noreferrer" target="_blank" href="https://www.notion.com/blog/how-we-sped-up-notion-in-the-browser-with-wasm-sqlite">blog post by Notion</a> as they have explained all the issues they were encountering and how they fixed it.</p>
<h2>Who uses sqlite3 WASM</h2>
<p>Some notable examples would be:</p>
<ol>
<li>Notion: as we already discussed in the post</li>
<li><a rel="noreferrer" target="_blank" href="https://sqlime.org/">SQLime</a>: an online SQLite playground that runs entirely in the browser</li>
<li><a rel="noreferrer" target="_blank" href="https://www.evolu.dev/">Evolu.dev</a>: a local-first platform designed for developers</li>
</ol>
<h2>Resources to learn more</h2>
<ul>
<li><a rel="noreferrer" target="_blank" href="https://www.sqlite.org/wasm/doc/trunk/index.md">sqlite3 WebAssembly & JavaScript Documentation Index</a></li>
<li><a rel="noreferrer" target="_blank" href="https://developer.chrome.com/blog/sqlite-wasm-in-the-browser-backed-by-the-origin-private-file-system">SQLite Wasm in the browser backed by the OPFS</a></li>
<li><a rel="noreferrer" target="_blank" href="https://news.ycombinator.com/item?id=33374402">A discussion about the same on HackerNews</a></li>
</ul>
<p>That's it.</p>
<p>I will be reading more about it, and will keep this post updated.</p>
Trying the LibreWolf Browser2025-04-18T02:03:55Zhttps://deepakness.com/blog/trying-librewolf-browser/Trying the LibreWolf web browser and will see if it becomes my default browser.<p>While browsing the <a rel="noreferrer" target="_blank" href="https://www.reddit.com/r/browsers/">r/browsers on Reddit</a>, I came across the name LibreWolf multiple times and then I finally decided to give it a try. Earlier, I tried the Ungoogled Chromium browser and it was unable to become my default browser <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/trying-ungoogled-chromium/">for certain reasons</a>.</p>
<p>This is not going to be a very technical post, instead, I will just be sharing my experience about what I liked and disliked. So... let's start from the start.</p>
<h2>1. Installation</h2>
<p>I am using Macbook Air M2 and the installation process seemed straightforward initially. I could install it through Homebrew by running the following command, or download the <code>.dmg</code> file and then install.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">brew <span class="token function">install</span> <span class="token parameter variable">--cask</span> librewolf</code></pre>
<p>I used the Homebrew method and it got installed after a minute. But when I tried opening the app, it didn't open and showed the following error dialog. For your information, currently, I'm on the latest macOS Sequoia 15.4.1.</p>
<p><img alt="Error while installing LibreWolf browser on macOS" decoding="async" src="https://assets.deepakness.com/blog/trying-librewolf-browser/librewolf-error.png"></p>
<p>I started looking and found this Reddit thread, tried a bunch of things, and finally found a solution. I have briefly <a rel="noreferrer" target="_blank" href="https://deepakness.com/raw/librewolf-browser-on-mac">explained about the same in this post</a>.</p>
<p>Finally, I got it to open.</p>
<h2>2. Things I liked</h2>
<p>I liked how clean it looked, but I went one step further and disabled <strong>shortcuts</strong> and <strong>recent activity</strong> from showing on the homepage and then looked even cleaner.</p>
<p><img alt="The LibreWolf web browser" decoding="async" src="https://assets.deepakness.com/blog/trying-librewolf-browser/librewolf-browser-1.png"></p>
<p>Now, let's go through the things I liked.</p>
<h3>Comes with uBlock Origin preinstalled</h3>
<p>Yes, I didn't even have to install an adblock extension as it already came with <a rel="noreferrer" target="_blank" href="https://github.com/gorhill/uBlock#ublock-origin">uBlock Origin extension</a> installed. The extension was also already pinned to the toolbar, as you can see in the top-right of the above screenshot.</p>
<p>In fact, after Google Chrome started enforcing the Manifest v3 for extensions, most of the Chromium-based browsers don't support the uBlock Origin out of the box now. And as LibreWolf is Firefox based, that's not an issue here with the extension.</p>
<h3>Supports Firefox sync</h3>
<p>It needs to be enabled from the settings, but the browser does come with the Firefox Sync feature. After enabling it, you can sign in to your Firefox Sync account or create a new account, and the sync starts working.</p>
<p><img alt="LibreWolf sync through Firefox Sync" decoding="async" src="https://assets.deepakness.com/blog/trying-librewolf-browser/librewolf-sync.png"></p>
<p>You can also copy-paste this <code>about:preferences#sync</code> in your address bar and it will take you the sync page. But obviously, it's optional.</p>
<h3>Vertical tabs</h3>
<p>I am not a fan of vertical tabs but I liked that it has the option to switch between horizontal and vertical tabs with just a click.</p>
<p><img alt="Horizontal and Vertical Tabs feature" decoding="async" src="https://assets.deepakness.com/blog/trying-librewolf-browser/horizontal-vertical-tabs.png"></p>
<p>For this, you'd go in the Browser Layout section on the <code>about:preferences#general</code> page, as shown above. From here, the sidebar can also be enabled.</p>
<h3>Forces web content to use light theme</h3>
<p>By default, the <code>ResistFingerprinting</code> feature is enabled so the browser doesn't let website see your device settings (whether it's on dark/light mode) and forces websites to show their content in the light mode only.</p>
<p><img alt="Website appearance" decoding="async" src="https://assets.deepakness.com/blog/trying-librewolf-browser/website-appearance.png"></p>
<p>I prefer light mode, so it's not a problem for me. But if you don't like, you will have to disable the feature and that will make the browser a bit less secure.</p>
<h3>Hardened security settings</h3>
<p>I really liked how the security on the browser is super-hardened out of the box.</p>
<p>For example, the "Delete cookies and site data when LibreWolf is closed" feature is turned on by default, means, all cookies will be cleared when you close the browser and you will be logged out from all websites. I don't need this, so I have disabled it.</p>
<p>Similarly, there are other options also that I liked:</p>
<ul>
<li>Multiple DNS over HTTPS providers</li>
<li>Ask to save passwords, addresses, and payment methods is turned off by default</li>
<li>HTTPS-only mode is enabled</li>
</ul>
<h3>Multiple profiles option</h3>
<p>Though the profiles option needs to be turned on manually, it's a great feature. I had to go to <code>about:config</code>, search for <code>browser.profiles.enabled</code>, and set it to <code>true</code>.</p>
<p><img alt="Multiple profiles in LibreWolf browser" decoding="async" src="https://assets.deepakness.com/blog/trying-librewolf-browser/librewolf-profiles.png"></p>
<p>I created 2 profiles – personal and work, and gave different colors to each so that they are easily identifiable. Also, I really liked the fact that both profiles open as separate apps, I have added them both in the Dock and can open any profile with just a click.</p>
<p>I found <a rel="noreferrer" target="_blank" href="https://www.reddit.com/r/LibreWolf/comments/1fep173/easy_way_to_switch_between_profiles/">this Reddit discussion</a> to be very helpful when doing all this.</p>
<h2>3. Things I didn't like</h2>
<p>Apart from the things liked, I also didn't like the fact that "profiles" option is not turned on by default.</p>
<p>That's it.</p>
<p><strong>Update:</strong></p>
<p>To be honest, security-wise, I liked LibreWolf the most out of all the browsers I have used as of now.</p>
<p>But I found the profile switching and Dock icons for different profiles to be buggy. For example, if I open two different profiles then I see two different icons in the Dock, great, but when I close both profile Windows, clicking on Dock icons do not open different profiles.</p>
<p>Sometimes, it becomes very confusing.</p>
<p>For now, I am sticking with the Brave browser, until I find something else.</p>
Trying the Ungoogled Chromium Browser2025-04-10T14:16:02Zhttps://deepakness.com/blog/trying-ungoogled-chromium/Was hearing a lot of good things about the Ungoogled Chromium browser, so gave it a go.<p>So... I tried switching to the <a rel="noreferrer" target="_blank" href="https://github.com/ungoogled-software/ungoogled-chromium">Ungoogled Chromium browser</a> as my main browser, instead of the Brave browser that I had been using before. To be honest, I sometimes find Brave’s push for crypto and AI a bit annoying, and that’s what made me look for an alternative. I’ve tried a few browsers before, so this time I thought I’d give Ungoogled Chromium a shot.</p>
<p>In this blog post, I will explain the installed process, first impressions, what I liked, what I disliked, and whether I will be using this as my main browser or not.</p>
<h2>The installation process</h2>
<p>I am, currently, using the MacBook Air M2 as my main computer and the installation process was very easy. I installed the <a rel="noreferrer" target="_blank" href="https://formulae.brew.sh/cask/ungoogled-chromium">ungoogled-chromium</a> version via Homebrew by running the following command in the terminal, which took around 30 seconds to complete.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">brew <span class="token function">install</span> <span class="token parameter variable">--cask</span> ungoogled-chromium</code></pre>
<p>I opened the browser, and was greeted by this <code>ungoogled-first-run</code> screen that you can see in the screenshot below. It looked very clean at first and I wasn't asked to log-in in order to sync my settings as Google Chrome normally does.</p>
<p><img alt="Ungoogled browser first run" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-1.jpeg"></p>
<p>Now, it was time to test it properly.</p>
<h2>Testing as per my use-cases</h2>
<h3>1. Changing the default search engine</h3>
<p>At first, there was no default search engine set so I opened settings and selected DuckDuckGo (which was pre-listed among among Bing and a few others, but no Google) as my default search engine. By the way, one can also add Google Search by clicking on the <code>Manage search engines and site search</code> button.</p>
<p><img alt="Changing the default search engine" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-2.jpeg"></p>
<h3>2. Installing Chrome extensions</h3>
<p>I visited Chrome Webstore and opened random extensions, but wasn't able to install as it was showing an error of "Item currently unavailable".</p>
<p><img alt="Can't install Chrome extensions" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-3.jpeg"></p>
<p>I directly visited the <a rel="noreferrer" target="_blank" href="https://github.com/gorhill/uBlock/releases">uBlock Origin's GitHub repo</a> and downloaded the <code>.zip</code> file for the Chromium build. The extension got installed with no errors and was working perfectly as well.</p>
<p><img alt="Way to install any Chrome extension" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-4.jpeg"></p>
<p>However, I noticed their instructions that explained a way that enables the Ungoogled browser to install any Chrome extension like you normally do. For that, I had to enable a flag in the <code>chrome://flags</code> and install a Chrome extension called <a rel="noreferrer" target="_blank" href="https://github.com/NeverDecaf/chromium-web-store">chromium-web-store</a>.</p>
<p><img alt="Enabling all Chrome extensions to install" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-5.jpeg"></p>
<p>After following the steps, "Add to Chrome" button started showing on all extensions from the Chrome Webstore, as you can see in the screenshot above. By the way, <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/creating-first-chrome-extension/">X Mute is a Chrome extension</a> that I have created – it can mute multiple words/phrases on X (Twitter) at once, and not to mention, it's free to use.</p>
<h3>3. Profiles and developer tools</h3>
<p>Somewhere I read that the Ungoogled Chromium browser doesn't support creating multiple profiles, but that's not true now. I successfully created two profiles named <code>personal</code> and <code>work</code>.</p>
<p><img alt="Developer tools" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-7.jpeg"></p>
<p>I also tried opening the developer tools and it worked as expected – just like how it does in the Google Chrome browser.</p>
<h3>4. Gmail and YouTube</h3>
<p>I also logged into Gmail and it does work, and it stays logged in even after I quit and re-opened the browser. YouTube works perfectly as well, no lags, or no missing features - theatre and fullscreen modes worked as well.</p>
<p><img alt="Gmail and YouTube" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-8.jpeg"></p>
<p>I also logged into X (Twitter) it also worked as expected.</p>
<h3>5. Secure DNS and Google</h3>
<p>I didn't expect that the "Use secure DNS" feature would be there in the browser, but it was there and worked as expected. I normally use the <a rel="noreferrer" target="_blank" href="https://one.one.one.one/">Cloudflare's 1.1.1.1 DNS resolver</a> which reduces the latency a bit and also makes everything neutral. I was able to turn it on and use it.</p>
<p><img alt="Secure DNS features" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-9.jpeg"></p>
<p>Also, I noticed a funny thing that it says "You and Google" but Google Sync doesn't work as it's disabled (which does make sense).</p>
<p><img alt="Secure DNS features" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-10.jpeg"></p>
<h3>6. DRM issues</h3>
<p>I tried opening a few Udemy courses to test the DRM features and it didn't work. Some courses played without any issues, but DRM protected courses didn't play, even after trying multiple times.</p>
<p><img alt="Secure DNS features" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-12.jpeg"></p>
<p>The workaround could have been to follow <a rel="noreferrer" target="_blank" href="https://ungoogled-software.github.io/ungoogled-chromium-wiki/faq#how-do-i-install-widevine-cdm">this tutorial on their FAQ page</a>, but I didn't have that much patience. It's something like, you'll have to download the Google Chrome official installer file, mount it, and run a terminal command to install the Widevine CDM.</p>
<h3>7. Benchmark testing</h3>
<p>I used the <a rel="noreferrer" target="_blank" href="https://browserbench.org/Speedometer3.1/">BrowserBench Speedometer to run tests</a> on the Ungoogled Chromium I installed and also on the Google Chrome browser that I already had installed. To my surprise, Google Chrome scored a bit higher than the Ungoogled browser.</p>
<p><img alt="BrowserBench Speedometer" decoding="async" src="https://assets.deepakness.com/blog/trying-ungoogled-chromium/browserbench-speedometer.jpg"></p>
<p>The left is Google Chrome stat (33.0) and the right is the Ungoogled Chromium stat (31.2). But I don't think, it's a deal breaker as the difference is not much.</p>
<h3>8. Experienced a bug</h3>
<p>Lastly, I experienced a weird bug as well. The uBlock Origin extension that I installed manually, automatically got uninstalled when the quit the browser using <code>cmd + q</code> and re-opened it. I installed the extension again and then it again got uninstalled.</p>
<p>This bug has now made it a difficult choice for me to permanently switch to this browser. But I am still testing it for a few days, and let's see if I change my mind.</p>
<p>For now, I will keep using the Brave browser.</p>
<p><strong>Update</strong>: Apr 18, 2025</p>
<p>After using the browser for more than a week, I have decided not to use this as my default browser, in fact, I have uninstalled it. And the main reasons are minor bugs that I mentioned, and also the very late security updates.</p>
<p>After this, I will be <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/trying-librewolf-browser/">trying the LibreWolf browser</a> and let's see if that meets my expectations.</p>
MP3Detective: AI-Powered Music Metadata Manager2025-04-06T02:31:03Zhttps://deepakness.com/blog/mp3detective/Learn how I built MP3Detective - a Python tool that uses AI to automatically fix MP3 metadata. Works with any language, tested with 95% accuracy.<p>I have a huge collection of MP3 files for Hindi songs, and most of them have incorrect or missing metadata (like song name, artist, album, etc.). I tried many online tools but none of them worked well, especially for old Bollywood songs.</p>
<p>So, I decided to build my own tool using OpenAI's GPT-4o model. And... say hello to <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/mp3detective">MP3Detective</a> – a Python tool that automatically finds and updates MP3 metadata.</p>
<p><img alt="MP3Detective Demo" decoding="async" src="https://assets.deepakness.com/blog/mp3detective/mp3detective-demo.png"></p>
<h2>How it works</h2>
<p>The tool is quite simple. You just:</p>
<ol>
<li>Put your MP3 files in the <code>input</code> folder</li>
<li>Run the Python script</li>
<li>Get your updated files in the <code>output</code> folder</li>
</ol>
<p>The best part? It works with any language or music genre. I tested it with my old Hindi songs collection, and it worked with about 95% accuracy. Yes, it did make some mistakes with very old or rare songs, but that's expected.</p>
<p>I also have a <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=sS35tyAsnYE">quick video</a> showing how it works:</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="sS35tyAsnYE" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="sS35tyAsnYE" style="background-image: url('https://i.ytimg.com/vi/sS35tyAsnYE/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<h2>Building the tool</h2>
<p>I used Python to build this tool because it has great libraries for handling MP3 files. The main pieces are:</p>
<ul>
<li>OpenAI's GPT-4o for identifying song details</li>
<li><code>eyeD3</code> for reading/writing MP3 tags</li>
<li><code>tqdm</code> for showing nice progress bars</li>
</ul>
<p>Here's the folder structure of how everything is placed:</p>
<pre class="language-plaintext" tabindex="0"><code class="language-plaintext">mp3detective/
├── .gitignore # Git ignore rules
├── app.py # Main application file
├── requirements.txt # Python dependencies
├── README.md # This documentation
├── input/ # Place your MP3 files here
└── output/ # Updated files will appear here</code></pre>
<p>The tool creates a copy of your original files, so you don't lose anything. It also keeps a detailed log of what it's doing, which helps if something goes wrong.</p>
<h2>Real-world testing</h2>
<p>I tried it with about 1,500 old Hindi songs first. Here's what I found:</p>
<ul>
<li>For most songs, it found the correct details</li>
<li>Some very old songs (1950s-60s) were tricky</li>
<li>Sometimes it couldn't decide between two similar song versions</li>
<li>The original files stayed safe in the input folder</li>
</ul>
<p>And this is what it shows in the terminal after the process finishes.</p>
<p><img alt="Terminal output after the task finishes" decoding="async" src="https://assets.deepakness.com/blog/mp3detective/terminal-output.png"></p>
<h2>Using it yourself</h2>
<p>Want to try it? It's pretty simple:</p>
<ol>
<li>Get the code <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/mp3detective">from GitHub</a></li>
<li>Install Python if you don't have it</li>
<li>Add your OpenAI API key</li>
<li>Put your MP3 files in the input folder</li>
<li>Run the script</li>
</ol>
<p>That's it! The tool will do its magic and you'll find your updated files in the output folder.</p>
<p>The code is open source, so feel free to try it out or even make it better!</p>
Running Ollama in Android Linux Terminal2025-03-18T13:47:49Zhttps://deepakness.com/blog/ollama-in-android-linux/I had to try running Ollama on the new Android Linux terminal and I did.<p>I had this random thought of running Ollama inside the <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/android-linux-terminal/">new Android Linux terminal</a>, so I tried running the small <code>gemma3:1b</code> model and it did run successfully. I also recorded a quick video which you can <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=Cgqg5bNyMHI">watch here</a>.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="Cgqg5bNyMHI" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="Cgqg5bNyMHI" style="background-image: url('https://i.ytimg.com/vi/Cgqg5bNyMHI/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>Now, let me document exactly what I did:</p>
<h2>1. Install Ollama</h2>
<p>I installed Ollama in the terminal by following their <a rel="noreferrer" target="_blank" href="https://ollama.com/download/linux">official guide</a> to install on Linux. Basically, I just ran the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://ollama.com/install.sh <span class="token operator">|</span> <span class="token function">sh</span></code></pre>
<p>At the first try, the install failed for unknown reasons, so I increased the disk size from the <strong>Disk Resize</strong> option under the gear icon in the terminal. And then running the above install command did work, for whatever reasons.</p>
<p>I, then, ran the <code>ollama</code> command and it showed me all the available options, as you can see in the screenshot here.</p>
<p><img alt="Running Ollama on the new Android Linux terminal" decoding="async" src="https://assets.deepakness.com/blog/ollama-in-android-linux/ollama-in-android-linux.png"></p>
<h2>2. Running the <code>gemma3:1b</code> model</h2>
<p>I looked for a lightweight model on <a rel="noreferrer" target="_blank" href="https://ollama.com/search">ollama.com/search page</a> and found the <code>gemma3:1b</code> to be a suitable one – it was 815MB in size which was manageable. First, I ran the <code>pull</code> command and then the <code>run</code> command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">ollama pull gemma3:1b</code></pre>
<pre class="language-bash" tabindex="0"><code class="language-bash">ollama run gemma3:1b</code></pre>
<blockquote>
<p>I did not directly run the <code>run</code> command because terminal was immediately crashing after running it.</p>
</blockquote>
<p>It took multiple tries, I had to run the following commands and then close-open the app multiple times:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">sudo</span> systemctl daemon-reload
<span class="token function">sudo</span> systemctl <span class="token builtin class-name">enable</span> ollama
<span class="token function">sudo</span> systemctl start ollama</code></pre>
<p>And then it finally started running...</p>
<pre class="language-plaintext" tabindex="0"><code class="language-plaintext">droid@localhost:~$ ollama run gemma3: 1b
>>> hi
Hi there! How's your day going so far?
Is there anything you'd like to chat about or any help I can offer?</code></pre>
<p>I said <code>hi</code> to which it replied as you can see above but the terminal crashed immediately after. I tried opening it again 2-3 times, but it didn't open. I had to erase all terminal data and reinstall Ollama, but then it crashed again after just a <code>hi</code>.</p>
<p>Clearly, the Android Linux terminal is not very stable at the moment but it's a good start. I am very hopeful about this, and I'm sure it will only improve in the future.</p>
<p><strong>Update:</strong></p>
<p>I got experiment some more and now Ollama is properly running for models like <code>qwen2.5:0.5b</code> and <code>qwen2.5-coder:0.5b</code> as they are significantly smaller than the <code>1b</code> models that I was trying to run earlier. I have also <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=xcNS9BBo2Gk">recorded a new video</a> for this:</p>
<div id="xcNS9BBo2Gk" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="xcNS9BBo2Gk" style="background-image: url('https://i.ytimg.com/vi/xcNS9BBo2Gk/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>I asked a bunch of questions, and the terminal is not crashing this time.</p>
Life with a Fractured Leg2025-03-16T07:35:45Zhttps://deepakness.com/blog/fractured-leg/Recently, I got my leg fractured while playing badminton so my life has been slow, but I'm loving it.<p>Almost 10 days ago, I suddenly got this idea to play badminton and asked my friend if he would join a court with me. He agreed, and we got ourselves admitted to a court nearby the same day, and started playing from the next day.</p>
<p>It was fun... while it lasted.</p>
<p>I got my leg fractured on the 3rd day after joining the court. Yes, it was my mistake but I was running towards the right to hit the shuttle and suddenly tripped while twisting my right leg. I felt an unbearable pain near my ankles and I sat down immediately.</p>
<p>I stayed there for some time, and then somehow got myself home with the help of my friend. And the visit to the doctor revealed that there's a minor fracture between the end of second and third toes (you can probably see that in the x-ray report below) – it's like zig-zag.</p>
<p><img alt="X-Ray report of my right foot" decoding="async" src="https://assets.deepakness.com/blog/fractured-leg/foot-x-ray.jpg"></p>
<p>I was scared a bit, but didn't think the doctor would recommend getting a plaster (cast) done to the leg. I thought, some crepe bandage would do the job, but... happened otherwise. They used some kind of synthetic cast tape which solidifies within minutes after applying. You can see how it looked at the time – just after the tape was applied.</p>
<p><img alt="Cast leg" decoding="async" src="https://assets.deepakness.com/blog/fractured-leg/plastered-fractured-leg.jpg"></p>
<blockquote>
<p>You know, it was funny that I wore tight pants to the doctor, and I had to cut them to remove after the cast was applied.</p>
</blockquote>
<p>Initially, it was very uncomfortable to even walk a step, but now I've gotten acquainted with it as it's been a few days. I can slowly walk to the washroom, kitchen, work setup room, and towards the main door for my daily works. It's just that I can't cook anymore and it's okay, Swiggy, Zomato, and BigBasket are of big help here.</p>
<p>But it has to go on for 30 days – another 25-26 days to be precise.</p>
<p>The good thing is, I can still work. I can comfortably sit in front of the computer for as long as I want, and do whatever (guess what, I'm using Cursor AI a lot these days). Life's been a bit slow, but it's giving me a lot of time to think:</p>
<ul>
<li>that how <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/everything-is-coincidence/">uncertain life is</a> – anything can happen to anyone</li>
<li>you do not always get what you plan for, so shouldn't take life that seriously</li>
</ul>
<p>But... I'm good.</p>
<p>Still learning new things, working, having fun, and enjoying this part of the life. Actually, I am sleeping a lot more these days and the health is good as well. Sometimes, I also play games on my phone especially the Mini Militia game, and loving it.</p>
<p>It seems like nothing bad has happened, I am already adapted to this new temporary style of life.</p>
<p>Clearly, whatever happens, life goes on!</p>
<p>And... I will be playing badminton again. For sure.</p>
Trying Out the New Android Linux Terminal2025-03-12T04:39:15Zhttps://deepakness.com/blog/android-linux-terminal/Tried running a basic Next.js app from the all-new Linux terminal in the Android – and it works.<p>I heard about <a rel="noreferrer" target="_blank" href="https://www.androidauthority.com/android-linux-terminal-app-3489887/">this news</a> of Google preparing to let you run Linux apps on Android back in October, 2024. And then, a few days ago, I learned that <a rel="noreferrer" target="_blank" href="https://news.itsfoss.com/google-android-linux-terminal-rollout/">it's actually live</a> on Android 15... so I had to try it out.</p>
<p>To enable this, I had to go into the <strong>Developer options</strong> in <strong>Settings</strong> and then enable the <strong>Linux development environment</strong> option. It downloads some ~500 MB of data before the terminal actually starts.</p>
<p><img alt="Screenshots from Android Native Linux Terminal" decoding="async" src="https://assets.deepakness.com/blog/android-linux-terminal/native-linux-terminal-android.png"></p>
<p>Yes, I installed <code>neofetch</code> and <code>htop</code> and you can see how it looks above. Cool, right?</p>
<p>And then I suddenly got an idea to install Node.js and run a basic Next.js app, and it just took me ~5 mins to install everything and the app was running. I even recorded the screen with a bit of commentary that you can <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=pSxusIH8o1E">watch here</a>.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="pSxusIH8o1E" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="pSxusIH8o1E" style="background-image: url('https://i.ytimg.com/vi/pSxusIH8o1E/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>At the very first, I installed the <code>nodejs</code> and <code>npm</code> by running below commands one by one (as you will see in the video above):</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> nodejs
<span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token function">npm</span></code></pre>
<p>I, then, directly ran the below command to set up a new Next.js application, and it worked. As it was just a test, I just chose all the default options and everything was ready.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">npx create-next-app@latest</code></pre>
<p>Lastly, I ran the <code>npm run dev</code> command inside the project folder and the app was running. I clicked on the Network link (<code>http://192.168.0.2:3000</code>) and the app opened (you'll my excitement in the video).</p>
<p>Now, let me address some <strong>common questions about the new Android Linux terminal</strong>:</p>
<h2>1. Can I install GUI on it?</h2>
<p>No, sadly, you cannot install any Desktop Environment on it as Google has already <a rel="noreferrer" target="_blank" href="https://www.androidpolice.com/google-terminal-linux-app-priorities/">made clear</a>. Actually, I tried running the <code>sudo tasksel</code> command but nothing happens.</p>
<h2>2. Can I install GUI apps on it?</h2>
<p>No, you can't run GUI apps either. For example, I installed the VS Code editor, but it doesn't open, even if you run the <code>code .</code> command inside a folder.</p>
<h2>3. What are the benefits of the native terminal over Termux?</h2>
<p>The only benefit that I see is that you can install packages directly from the <code>sudo apt install <package name></code> command. You have access to 10,000 or even more apps (but only terminal apps/tools will actually run).</p>
<p>And if you want to learn more about this, I found a <a rel="noreferrer" target="_blank" href="https://www.threads.net/@mishaal_rahman/post/DCZorPpvv-C">detailed post discussing this</a> that you can refer to.</p>
<h2>4. How's the opening speed?</h2>
<p>It's slow – takes somewhere from 10 seconds to 20 seconds on my Pixel 7a device. Unlike Termux, it runs using KVM and that's the reason it's slow.</p>
<h2>5. Can the terminal access Android files?</h2>
<p>It can access your Android's device downloads folder, but cannot access other folders or files. To access the download folder, you need to run the below command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">cd</span> /mnt/shared</code></pre>
<p>I have created <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=NISaDcmgKw0">another video</a> where I am using the Linux terminal and showing what it can do and what it cannot. It's a quick fun video, so do give it a watch.</p>
<div id="NISaDcmgKw0" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="NISaDcmgKw0" style="background-image: url('https://i.ytimg.com/vi/NISaDcmgKw0/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>Also, I tried running local AI model <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/ollama-in-android-linux/"><code>gemma3</code> via Ollama</a> in the terminal, and it runs.</p>
<p><strong>Update</strong>: Apr 4, 2025</p>
<p>I received an email from a person asking about an issue that wasn't able to install any tools. For example, the person was trying to install <code>neofetch</code> and it was constantly showing errors, as you can see explained in the screenshot.</p>
<p><img alt="Email about Android Terminal" decoding="async" src="https://assets.deepakness.com/blog/android-linux-terminal/email-from-a-person.png"></p>
<p>After exchanging a few emails, we finally learnt that the terminal didn't have internet access when the device was connected to the cellular data. And connecting to the wifi did solve the problem.</p>
<p><strong>Update</strong>: Apr 15, 2025</p>
<p>I received another email where the person got the internet working by using the cellular data, below is how you can do it too.</p>
<p>To access the internet when the device is connected to the cellular data, you need to go to Terminal's Settings (long press on the Terminal app icon and then click on the <code>i</code> button), and turn on the <strong>Unrestricted mobile data usage</strong> under Mobile data usage.</p>
<p><img alt="Terminal settings for cellular data usage" decoding="async" src="https://assets.deepakness.com/blog/android-linux-terminal/terminal-settings-for-cellular-data.png"></p>
<p>The setting might be at a different location depending on your device, but basically, you have to unrestrict the mobile data usage. Then <strong>restart the device</strong>, and it should work.</p>
<p>That's it.</p>
<p>It's just the start, and I'm sure the Linux thing will become even more powerful in the near future. I am really really excited for it.</p>
Optimizing 1,300+ Images using 'sharp'2025-03-08T05:06:12Zhttps://deepakness.com/blog/optimizing-images/Had over 1300 PNG images that I needed to optimize for the web, and wrote a Node.js script that optimized them all in 2 mins.<p>I found out about the <a rel="noreferrer" target="_blank" href="https://sharp.pixelplumbing.com/">"sharp" image library</a> when I was migrating my website from <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/from-wordpress-to-11ty/">WordPress to 11ty</a>, as it was mentioned somewhere on the 11ty documentation page. And coincidentally, only a few days later, I had to optimize over 1,300 images for a project and I immediately moved to using <code>sharp</code>.</p>
<p>I even created a <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/optisharp">GitHub repository</a> (have named it OptiSharp) and have put all the code there. You can also watch this <a rel="noreferrer" target="_blank" href="https://youtu.be/s8umFi6iQWA">quick video</a> to understand how it works.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="s8umFi6iQWA" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="s8umFi6iQWA" style="background-image: url('https://i.ytimg.com/vi/s8umFi6iQWA/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<h2>How OptiSharp works</h2>
<p>OptiSharp is a simple tool that makes your images smaller and better for the web. Here's how it works:</p>
<p>#1. <strong>Put your images in</strong>: Just drop all your images into the <code>input</code> folder.</p>
<p>#2. <strong>Choose your settings</strong>: You can easily change a few settings like:</p>
<ul>
<li>How small you want the images to be</li>
<li>What type of image you want (JPEG, PNG, WebP, etc.)</li>
<li>How good the quality should be</li>
<li>If you want to keep the image information (like camera details)</li>
</ul>
<p>#3. <strong>Run the tool</strong>: Type one command, and it will:</p>
<ul>
<li>Look at all your images</li>
<li>Make them smaller</li>
<li>Save them in the <code>output</code> folder</li>
<li>Tell you how much space you saved</li>
</ul>
<p>#4. <strong>Get results</strong>: After it's done, you'll see:</p>
<ul>
<li>How many images it processed</li>
<li>How much smaller they are now</li>
<li>How long it took</li>
<li>If there were any problems</li>
</ul>
<h2>Why I made this</h2>
<p>I needed to make lots of images smaller for websites, and doing it one by one was taking too long. Most online tools:</p>
<ul>
<li>Only let you do a few images at a time</li>
<li>Have size limits</li>
<li>Sometimes make you pay</li>
<li>Don't give you many options</li>
</ul>
<p>OptiSharp solves these problems because it:</p>
<ul>
<li>Can handle hundreds of images at once</li>
<li>Works on your own computer</li>
<li>Is completely free</li>
<li>Lets you control exactly how you want to change your images</li>
</ul>
<h2>How to use it</h2>
<ol>
<li>Make sure you have Node.js on your computer</li>
<li><a rel="noreferrer" target="_blank" href="https://github.com/deepakness/optisharp/archive/refs/heads/main.zip">Download</a> OptiSharp from GitHub</li>
<li>Put your images in the input folder</li>
<li>Run the program</li>
<li>Get your optimized images from the output folder</li>
</ol>
<p>It's that simple! The tool will show you how much space you saved and if there were any problems.</p>
<h2>What's next?</h2>
<p>I'm planning to add more features like:</p>
<ul>
<li>A simple screen where you can change settings without coding</li>
<li>More ways to make images look better</li>
<li>Ability to rename files automatically</li>
</ul>
<p>If you want to try OptiSharp, visit the <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/optisharp">GitHub page</a> and follow the instructions. It's free and open for everyone to use and improve!</p>
Taking Notes in VS Code2025-03-04T16:11:13Zhttps://deepakness.com/blog/taking-notes-in-vs-code/I started using VS Code for taking notes a while ago, and I am loving it now.<p>I have tried many different tools and apps for taking notes, but recently I started using VS Code to take notes as markdown files, and I am really enjoying it. I <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1880805559404019905">tweeted about it</a> when I started and now it's been more than a month, and I'm still use the notetaking setup.</p>
<div class="eleventy-plugin-embed-twitter"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">While I use Notion to manage most of my "business" stuff, I've started taking plaintext notes in Markdown format for my "personal" stuff.<br><br>Simply using the VS Code editor for this, along with a few tools:<br><br>1. Markdown All in One – for better Markdown support when editing<br><br>👇 <a rel="noreferrer" target="_blank" href="https://t.co/Scr4YGO6nr">pic.twitter.com/Scr4YGO6nr</a></p>— DeepakNess (@DeepakNesss) <a rel="noreferrer" target="_blank" href="https://twitter.com/DeepakNesss/status/1880805559404019905?ref_src=twsrc%5Etfw">January 19, 2025</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</div>
<p>In the past, I have used tools like:</p>
<ul>
<li>Notion</li>
<li>Evernote</li>
<li>Standard Notes</li>
<li>Simple Note</li>
<li>Logseq</li>
<li>Joplin</li>
<li><a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/still-obsidian/">Obsidian</a></li>
<li>Notion, and more.</li>
</ul>
<p>And now using the simple VS Code and organizing them by creating different folders and files as needed. I love Obsidian as well, but honestly, I got bored and just wanted to try a new setup.</p>
<p>Now, let me showcase my setup, including how I am using it.</p>
<h2>The setup</h2>
<p>My current setup is simple, I have a folder opened in the VS Code editor where I create <code>.md</code> files for each new topic and then write everything in plaintext, as you might be seeing in the screenshot below. For images and other files, I have a folder called <code>files</code> where I can copy-paste any kind of file that I want to refer to in my notes.</p>
<p><img alt="A screenshot of the note-taking setup in the VS Code editor" decoding="async" src="https://assets.deepakness.com/blog/taking-notes-in-vs-code/vs-code-notes-setup.png"></p>
<p>I do not use Copilot's AI features while writing, because I believe that <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/write-today-edit-tomorrow/">writing should be distraction free</a> and getting auto-completions and suggestions are distracting. But I do use some extensions to make my writing experience better:</p>
<h3>1. Code Spell Checker</h3>
<p>Sometimes, I also publish some of notes online so I can't have minor spelling errors. And that's why extension called Code Spell Checker comes into play. It doesn't distract, but firmly highlights words with incorrect spellings.</p>
<h3>2. GitDoc</h3>
<p>All notes are offline, but can't afford to lose them and this extension helps me automatically push new changes to a private GitHub repo every 30 seconds.</p>
<h3>3. Markdown All in One</h3>
<p>This extension is the best, it simplifies the markdown writing process for me. For example, if I type <code>-</code> to get into the unordered list mode, it detects that and pressing <code>Enter</code> creates a new list item. It's also super helpful for creating Markdown tables.</p>
<p>Apart from these, I have Atom Material Icons and Theme installed in the VS Code which I like a lot.</p>
Creating My First Chrome Extension2025-02-22T15:01:41Zhttps://deepakness.com/blog/creating-first-chrome-extension/Sharing my complete experience of building my very first Chrome extension and submitting to the Chrome Webstore.<p>I am not a developer but I tried building a Chrome extension 4 years ago, but couldn't complete the project as I encountered so many errors and issues. But the idea of creating simple Chrome extensions stayed with me.</p>
<p>And... allow me to introduce <a rel="noreferrer" target="_blank" href="https://mute.deepakness.com/">X Mute</a> – a <a rel="noreferrer" target="_blank" href="https://chromewebstore.google.com/detail/X%20Mute/linlakoepnjkdiphemdpbphnjlppjedm">Chrome extension</a> that helps you clean your X (Twitter) feed by allowing you to mute multiple words at once. Currently, you need to mute words one-by-one on X, but the extension can block tens of words in no time.</p>
<p><img alt="A screenshot of the X Mute Chrome extension" decoding="async" src="https://assets.deepakness.com/blog/creating-first-chrome-extension/x-mute-chrome-extension.png"></p>
<p>As you can see in the above screenshot, you can copy-paste multiple comma-separated words/phrases at once, select right settings and just click the <strong>Start Muting</strong> button. It mutes all the provided words one-by-one.</p>
<h2>Getting started with building</h2>
<p>A few days ago, I <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1891069763184697499">tweeted this</a> idea and started building it by myself a few days later (yes, this post is about that only).</p>
<div class="eleventy-plugin-embed-twitter"><blockquote class="twitter-tweet"><p lang="en" dir="ltr">IDEA:<br>Create a Chrome extension that allows you to add a list of words or phrases and automatically adds all of them to the mute list.<br><br>Currently, you have to mute words one by one. <a rel="noreferrer" target="_blank" href="https://t.co/noe63CM0Af">https://t.co/noe63CM0Af</a></p>— DeepakNess (@DeepakNesss) <a rel="noreferrer" target="_blank" href="https://twitter.com/DeepakNesss/status/1891069763184697499?ref_src=twsrc%5Etfw">February 16, 2025</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</div>
<p>But this time, I took help of AI to build the extension – the mighty Cursor AI. It took me a few hours to to complete the initial version, but it was worth it. You can see the screenshot of my Cursor IDE setup.</p>
<p><img alt="Cursor AI setup for X Mute Chrome extension" decoding="async" src="https://assets.deepakness.com/blog/creating-first-chrome-extension/x-mute-cursor.png"></p>
<p>In the first prompt itself, I provided the HTML snippets of <a rel="noreferrer" target="_blank" href="https://x.com/settings/muted_keywords">X page to mute words</a> so that Cursor can auto-select the required CSS Selectors from the code, and it did that. The basic version of the extension was ready in the first two hours itself, and then I spent the next few hours tweaking the code to make polish the UI and UX a bit.</p>
<p>The same day, I even created the Chrome developer account and tried getting myself verified by paying $5 and submitting my IDs. They said that they'll get back in the next few days, so then I started working on the landing page that I, again, created using Cursor. It took me around 1.5 hours to complete the landing page using Next.js, and I chose Next.js because the <a rel="noreferrer" target="_blank" href="https://mute.deepakness.com/words">mute words list project</a> was already built using the same tech stack.</p>
<p>Then, I called it a day and started waiting for the next day... to get verified.</p>
<h2>Submitting to Chrome Webstore</h2>
<p>The very next day, my Chrome Webstore developer account actually got verified and I received the below confirmation email.</p>
<p><img alt="Chrome Webstore verification confirmation email" decoding="async" src="https://assets.deepakness.com/blog/creating-first-chrome-extension/chrome-webstore-verification.png"></p>
<p>The next step was to submit the extension as a <code>.zip</code> file along with proper description, screenshots, and logo.</p>
<ol>
<li>For the screenshot, I just opened the "popup.html" in Chrome and did some minor CSS changes to make it appear in the middle and then took the screenshot.</li>
<li>For the description, I asked Cursor itself to generate a solid description for the extension and then copy-pasted the same while submitting. I also had to provide <strong>Permission justification</strong> which Cursor wrote for me.</li>
<li>For the logo, I got this flower like SVG icon from <a rel="noreferrer" target="_blank" href="https://www.reshot.com/">Reshot</a> and just edited the color to match the website's theme.</li>
</ol>
<p>Then I hit the <strong>Submit for review</strong> button and started waiting for the good news.</p>
<h2>Getting rejected</h2>
<p>The very next day, I received an email (see screenshot) saying that the submission was rejected due to some kind of violations.</p>
<p><img alt="Extension submission got rejected" decoding="async" src="https://assets.deepakness.com/blog/creating-first-chrome-extension/chrome-webstore-rejection-email.png"></p>
<p>Upon looking, I discovered that I was requesting an additional "scripting" permission in the <code>manifest.json</code> file but that permission wasn't getting used for anything. I provided the rejection email to my developer, Cursor, and asked it to fix the issue. Cursor also agreed that the "scripting" permission was unnecessary and can be safely removed without harming any of the features.</p>
<p>I re-submitted the fresh <code>.zip</code> file and started waiting for the updated. I kept refreshing my emails but didn't receive a reply for the next few days.</p>
<h2>Live on Chrome Webstore</h2>
<p>After 4 days, I received the email (see screenshot) that I was waiting for – my submission was accepted and the <a rel="noreferrer" target="_blank" href="https://chromewebstore.google.com/detail/X%20Mute/linlakoepnjkdiphemdpbphnjlppjedm">extension was live on the Webstore</a>.</p>
<p><img alt="Email notification after Chrome extension got published" decoding="async" src="https://assets.deepakness.com/blog/creating-first-chrome-extension/successfully-approved-and-published.png"></p>
<p>It's just the very first version of the extension, and I will be further improving it by adding more features and making it even more user friendly.</p>
<p>And now that I have a little bit of understanding how Chrome extensions are developed, expect more cool extensions coming from my side.</p>
<p>That's it.</p>
Better YouTube Embeds ~77% Faster2025-02-14T17:35:50Zhttps://deepakness.com/blog/better-youtube-embed/YouTube videos were significantly slowing down posts, so discovered this better technique.<p>I was using the <code>iframe</code> to embed YouTube videos to <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog">my blog posts</a>, and it was significantly slowing down the website. I started looking for a way to optimize the loading of embedded videos and have finally found a solution.</p>
<p>But before I start explaining, first, take a look at the GTMetrix loading chart <strong>before</strong> I implemented the solution. It was taking 3.6 seconds to fully load the webpage, which is just too much.</p>
<p><img alt="Slow loading with normal YouTube embed" decoding="async" src="https://assets.deepakness.com/blog/better-youtube-embed/before.jpeg"></p>
<p>But <strong>after</strong> I implemented the solution, the same webpage now takes 0.78 seconds – <strong>almost 77% faster than earlier</strong>.</p>
<p><img alt="YouTube embed – fast loading" decoding="async" src="https://assets.deepakness.com/blog/better-youtube-embed/after.jpeg"></p>
<h2>Introducing <code>lite-youtube-embed</code></h2>
<p>While looking for a solution, I came across this <a rel="noreferrer" target="_blank" href="https://github.com/paulirish/lite-youtube-embed">GitHub repo</a> called <code>lite-youtube-embed</code> that had the exact solution what I looking for. And then I found out that someone has also created a 11ty <code>npm</code> package <a rel="noreferrer" target="_blank" href="https://www.npmjs.com/package/eleventy-plugin-embed-everything">for the same</a> that I can install and use (for context, <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/from-wordpress-to-11ty">I recently moved this site to 11ty</a>).</p>
<blockquote>
<p>Initially, it only loads the video thumbnail with an YouTube play icon over it, and necessary scripts are only loaded someone clicks on the video. So... this makes the initial loading faster.</p>
</blockquote>
<p>The installation process was fairly easy, first, I just ran the following npm install command and then added it to the Eleventy config file.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">npm</span> i eleventy-plugin-embed-everything</code></pre>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token comment"># Import at the top</span>
<span class="token function">import</span> embedEverything from <span class="token string">"eleventy-plugin-embed-everything"</span><span class="token punctuation">;</span>
<span class="token comment"># Add below somewhere inside "export default async function(eleventyConfig)" function</span>
eleventyConfig.addPlugin<span class="token punctuation">(</span>embedEverything, <span class="token punctuation">{</span>
youtube: <span class="token punctuation">{</span>
options: <span class="token punctuation">{</span>
lite: <span class="token boolean">true</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>And... it was working.</p>
<blockquote>
<p>Make sure, <code>lite</code> is set to <code>true</code> as you see above.</p>
</blockquote>
<p>Earlier, I had to manually copy the <code>iframe</code> embed code from YouTube in my <code>.md</code> file which looked like this:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token operator"><</span>iframe width<span class="token operator">=</span><span class="token string">"560"</span> height<span class="token operator">=</span><span class="token string">"315"</span> src<span class="token operator">=</span><span class="token string">"https://www.youtube.com/embed/H21-e-zy-b8?si=eJzwJ0Xfc53dxy7y"</span> title<span class="token operator">=</span><span class="token string">"YouTube video player"</span> frameborder<span class="token operator">=</span><span class="token string">"0"</span> allow<span class="token operator">=</span><span class="token string">"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"</span> referrerpolicy<span class="token operator">=</span><span class="token string">"strict-origin-when-cross-origin"</span> allowfullscreen<span class="token operator">></span><span class="token operator"><</span><span class="token operator">/</span>iframe<span class="token operator">></span></code></pre>
<p>But now?</p>
<pre class="language-markdown" tabindex="0"><code class="language-markdown"><span class="token title important"><span class="token punctuation">##</span> Lorem ipsum dolor sit amet.</span>
https://www.youtube.com/watch?v=H21-e-zy-b8
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a urna ut massa tempus varius sit amet vel eros.</code></pre>
<p>I can just paste any YouTube video URL directly in the Markdown, whether it's short or full URL, and it gets embedded.</p>
I'm NOT Blocking AI Crawlers, For Now2025-02-12T05:39:31Zhttps://deepakness.com/blog/block-ai-crawlers/Clearing the confusion of whether one should block AI crawlers on their websites or not.<p>During the middle of last year, Cloudflare introduced a new button called <strong>"Block AI Bots"</strong>, designed to block all AI crawlers. It’s available even on their free-tier plan, and you can enable it with just one click.</p>
<p><img alt="Cloudflare Block AI Button" decoding="async" src="https://assets.deepakness.com/blog/block-ai-crawlers/cloudflare-block-ai-bots.png"></p>
<p>I love it! But I'm not using it.</p>
<p>While I think it’s a great feature, I’ve decided not to enable it on my websites—and I have good reasons. AI assistants like ChatGPT are already driving a significant amount of traffic to my projects. For example, the website shown in the Google Analytics screenshot below receives around <strong>450 visits per month</strong> from ChatGPT alone, and I’m happy with that.</p>
<p><img alt="" decoding="async" src="https://assets.deepakness.com/blog/block-ai-crawlers/chatgpt-traffic-source.png"></p>
<h2>The future of AI and content</h2>
<p>Looking ahead, I believe avoiding new technologies will only hurt us rather than help us. AI is one such technology that we shouldn’t resist—we should embrace and adapt to it.</p>
<blockquote>
<p>However, if you create content that takes a lot of effort and you want to keep it exclusive to your audience, then blocking AI bots might be the right choice for you.</p>
</blockquote>
<p>There’s also an ethical concern that if AI companies are <strong>"stealing"</strong> copyrighted content without permission, that’s a serious issue. We need a proper system where authors receive due credit if AI models use their work.</p>
<p>For now, I’m allowing AI crawlers because they benefit me. But if I find out in the future that they’re using my content without proper attribution, I won’t hesitate to block them.</p>
Create New Posts Faster in 11ty2025-02-06T14:19:26Zhttps://deepakness.com/blog/create-posts-faster-11ty/A quick guide on how to create new posts faster in 11ty.<p>I recently moved my personal blog <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/from-wordpress-to-11ty/">from WordPress to 11ty</a> and also documented the entire process in <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=H21-e-zy-b8">a video</a>. I like the current setup in 11ty, but there's an issue...</p>
<p>It takes me a lot of time to create a new blog post, as my blog posts are organized in <code>content/blog/<new-post></code> folders, as you see in the below screenshot.</p>
<p><img alt="Current blog posts organization" decoding="async" src="https://assets.deepakness.com/blog/create-posts-faster-11ty/blog-posts-organization.jpeg"></p>
<p>As of now, just to create a new blog post, I have to open multiple folders, create the markdown file, and the write all frontmatter properties. And it takes a lot of time.</p>
<p>So... I asked Cursor AI to create a Node.js script that can do all that with just a single command. If you prefer, you can watch <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=UNrFBDnrz40">this video</a> or continue reading below to learn more.</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="UNrFBDnrz40" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="UNrFBDnrz40" style="background-image: url('https://i.ytimg.com/vi/UNrFBDnrz40/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>I just asked Cursor composer to create a Node.js script where I can run something like <code>node create.js new-post</code> and it should automatically create a new file <code>index.md</code> in the specified sub-folder in the <code>content/blog</code> folder. And also asked it to auto-populate the frontmatter properties like title, description, date, and tags.</p>
<p>If you want, below was the exact prompt that you will also see in the above video:</p>
<pre class="language-plaintext" tabindex="0"><code class="language-plaintext">Write a node.js script that creates new blog posts in content/blog folder.
node create.js rise-of-cursor
if i run a command like above, it should create a new folder in the content/blog folder and then create a index.md file and then populate title, description, tags, and date properties for the frontmatter.</code></pre>
<p>When I ran the terminal command, it gave me an error which I just copied-pasted to Cursor and asked it to fix the issue. And then it provided me with the working script, which is below:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span> fs <span class="token keyword">from</span> <span class="token string">'fs'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> path <span class="token keyword">from</span> <span class="token string">'path'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> fileURLToPath <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'url'</span><span class="token punctuation">;</span>
<span class="token comment">// Get the current directory using ES modules</span>
<span class="token keyword">const</span> __filename <span class="token operator">=</span> <span class="token function">fileURLToPath</span><span class="token punctuation">(</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> __dirname <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">dirname</span><span class="token punctuation">(</span>__filename<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Get the slug from command line arguments</span>
<span class="token keyword">const</span> slug <span class="token operator">=</span> process<span class="token punctuation">.</span>argv<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>slug<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Please provide a slug for the blog post'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Example: node create.js my-new-post'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
process<span class="token punctuation">.</span><span class="token function">exit</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Create the blog post directory and content</span>
<span class="token keyword">const</span> blogDir <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">'content'</span><span class="token punctuation">,</span> <span class="token string">'blog'</span><span class="token punctuation">,</span> slug<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> indexFile <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>blogDir<span class="token punctuation">,</span> <span class="token string">'index.md'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Create directory if it doesn't exist</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>blogDir<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
fs<span class="token punctuation">.</span><span class="token function">mkdirSync</span><span class="token punctuation">(</span>blogDir<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">recursive</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Generate the frontmatter content</span>
<span class="token keyword">const</span> title <span class="token operator">=</span> slug
<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'-'</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">word</span> <span class="token operator">=></span> word<span class="token punctuation">.</span><span class="token function">charAt</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> word<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> now <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> date <span class="token operator">=</span> now<span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">'T'</span><span class="token punctuation">,</span> <span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">19</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">'+00:00'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> content <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">---
date: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>date<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
tags:
- blog
title: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
description: Add your description here
---
Write your blog post content here...
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token comment">// Write the file</span>
fs<span class="token punctuation">.</span><span class="token function">writeFileSync</span><span class="token punctuation">(</span>indexFile<span class="token punctuation">,</span> content<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">✨ Blog post created successfully at: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>indexFile<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'You can now edit the file to add your content!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>It auto-captures the current date-time as well for the date property, and fills in dummy data for other properties. Now, I can just edit the <code>index.md</code> file and publish a lot faster than earlier.</p>
<p>I think, there might already be a better way to do the same, but this solution is not unproductive either.</p>
<h2>Update:</h2>
<p>I further enhanced the script to make the workflow even faster. Earlier, once the file is created, I had to manually look through the folders to find the newly created file to start editing; but now, the script also opens the file for editing as soon as it gets created.</p>
<div id="HCah_XpcOAg" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="HCah_XpcOAg" style="background-image: url('https://i.ytimg.com/vi/HCah_XpcOAg/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>I have <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=HCah_XpcOAg">created a video</a> for the same and have shown how the script works. If you want, below is the complete script:</p>
<pre class="language-javascript" tabindex="0"><code class="language-javascript"><span class="token keyword">import</span> fs <span class="token keyword">from</span> <span class="token string">'fs'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> path <span class="token keyword">from</span> <span class="token string">'path'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> fileURLToPath <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'url'</span><span class="token punctuation">;</span>
<span class="token keyword">import</span> <span class="token punctuation">{</span> spawn <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">'child_process'</span><span class="token punctuation">;</span>
<span class="token comment">// Get the current directory using ES modules</span>
<span class="token keyword">const</span> __filename <span class="token operator">=</span> <span class="token function">fileURLToPath</span><span class="token punctuation">(</span><span class="token keyword">import</span><span class="token punctuation">.</span>meta<span class="token punctuation">.</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> __dirname <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">dirname</span><span class="token punctuation">(</span>__filename<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Get the slug from command line arguments</span>
<span class="token keyword">const</span> slug <span class="token operator">=</span> process<span class="token punctuation">.</span>argv<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>slug<span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Please provide a slug for the blog post'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">'Example: node create.js my-new-post'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
process<span class="token punctuation">.</span><span class="token function">exit</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Create the blog post directory and content</span>
<span class="token keyword">const</span> blogDir <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">'content'</span><span class="token punctuation">,</span> <span class="token string">'blog'</span><span class="token punctuation">,</span> slug<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> indexFile <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span>blogDir<span class="token punctuation">,</span> <span class="token string">'index.md'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Create directory if it doesn't exist</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>fs<span class="token punctuation">.</span><span class="token function">existsSync</span><span class="token punctuation">(</span>blogDir<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
fs<span class="token punctuation">.</span><span class="token function">mkdirSync</span><span class="token punctuation">(</span>blogDir<span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">recursive</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// Generate the frontmatter content</span>
<span class="token keyword">const</span> title <span class="token operator">=</span> slug
<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'-'</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">map</span><span class="token punctuation">(</span><span class="token parameter">word</span> <span class="token operator">=></span> word<span class="token punctuation">.</span><span class="token function">charAt</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toUpperCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> word<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> now <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> date <span class="token operator">=</span> now<span class="token punctuation">.</span><span class="token function">toISOString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token string">'T'</span><span class="token punctuation">,</span> <span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">19</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">'+05:30'</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> content <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">---
date: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>date<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">
tags:
- interesting
title: "</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>title<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">"
description: "Add your description here"
---
Write your blog post content here...
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span>
<span class="token comment">// Write the file</span>
fs<span class="token punctuation">.</span><span class="token function">writeFileSync</span><span class="token punctuation">(</span>indexFile<span class="token punctuation">,</span> content<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">✨ Blog post created successfully at: </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>indexFile<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'You can now edit the file to add your content!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// Open the file in VS Code</span>
<span class="token keyword">const</span> absolutePath <span class="token operator">=</span> path<span class="token punctuation">.</span><span class="token function">resolve</span><span class="token punctuation">(</span>indexFile<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">spawn</span><span class="token punctuation">(</span><span class="token string">'code'</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">'--goto'</span><span class="token punctuation">,</span> absolutePath<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">stdio</span><span class="token operator">:</span> <span class="token string">'inherit'</span><span class="token punctuation">,</span>
<span class="token literal-property property">shell</span><span class="token operator">:</span> process<span class="token punctuation">.</span>platform <span class="token operator">===</span> <span class="token string">'win32'</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Opening the file...'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>You can edit it as per your requirements and get it working. It will at least save you 30 seconds of your valuable time.</p>
Migrating from WordPress to 11ty2025-02-03T00:00:00Zhttps://deepakness.com/blog/from-wordpress-to-11ty/Documenting the process of moving my personal site having 100s of posts from WordPress to 11ty.<p>I have been using WordPress for a long time for my personal website. But these days, I love writing and taking <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1880805559404019905">notes inside VS Code</a> so thought to move my personal blog from WordPress to the <a rel="noreferrer" target="_blank" href="https://11ty.dev/">11ty SSG</a>.</p>
<p>The issue was, I had over 130 blog posts and manually converting them to Markdown and moving them to 11ty wasn't possible. But... I finally got it working, and you're reading this blog post on my new 11ty blog.</p>
<p>So... let me explain the entire process from the start. Here's a <a rel="noreferrer" target="_blank" href="https://youtu.be/H21-e-zy-b8">video that I recorded</a>:</p>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.css">
<script defer="defer" src="https://cdn.jsdelivr.net/gh/paulirish/[email protected]/src/lite-yt-embed.min.js"></script>
<div id="H21-e-zy-b8" class="eleventy-plugin-youtube-embed"><lite-youtube videoid="H21-e-zy-b8" style="background-image: url('https://i.ytimg.com/vi/H21-e-zy-b8/hqdefault.jpg');" params="rel=0"><div class="lty-playbtn"></div></lite-youtube></div>
<p>You can continue below, if you prefer to read instead.</p>
<h2>1. Started with a 11ty starter</h2>
<p>Initially, I thought of building the site from the scratch, but later I decided to use the <a rel="noreferrer" target="_blank" href="https://demo-base-blog.11ty.dev/">official 11ty starter</a>. I chose the starter theme and didn't build from scratch because it has a lot of featured already built-in, such as:</p>
<ul>
<li>Tags system</li>
<li>XML sitemap</li>
<li>RSS feed</li>
<li>Image optimization</li>
<li>Better navigation</li>
<li>Fast loading, etc.</li>
</ul>
<p>If I started from the scratch then it would have taken a lot more time than it did to customize the starter theme as per my requirements.</p>
<h2>2. Customizing the starter theme</h2>
<p>Once the theme was finalized, I started customizing and tweaking a few things in the theme. I didn't change a lot of things, though. In fact, let me explain what customizations I did.</p>
<h3>Added my photo in the header</h3>
<p>Adding the photo in the header was the most tricky part, because the pre-installed <a rel="noreferrer" target="_blank" href="https://www.11ty.dev/docs/plugins/image/">11ty Image</a> plugin was automatically optimizing the image and changing the image URL, and the image was broken in production. I spent a lot of time debugging the issue, and finally settled with adding <code>eleventy:ignore</code> in the HTML, as shown below:</p>
<pre class="language-html" tabindex="0"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>a</span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>home-link<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>img</span> <span class="token attr-name"><span class="token namespace">eleventy:</span>ignore</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>/img/deepakness.jpg<span class="token punctuation">"</span></span> <span class="token attr-name">alt</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>DeepakNess<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
DeepakNess
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>a</span><span class="token punctuation">></span></span></code></pre>
<p>I could also have removed the image transform plugin, but I didn't want to as it's an amazing tool to optimize images.</p>
<h3>Added social links and nav in the footer</h3>
<p>Adding the footer navigation was easy, but I had to try multiple different icon community plugins. I finally settled with <a rel="noreferrer" target="_blank" href="https://www.npmjs.com/package/eleventy-plugin-phosphoricons">eleventy-plugin-phosphoricons</a> as it had all social media and multiple different icons available. Below is how it looks in action:</p>
<p><img alt="Phosphor icons in use" decoding="async" src="https://assets.deepakness.com/blog/from-wordpress-to-11ty/phospohor-icons-in-use.png"></p>
<p>Actually, I used Lucide icons as well, but didn't have the new icon for X (Twitter) so I had to look for an alternative, and Phosphor Icons was the best.</p>
<h3>Other minor customizations</h3>
<p>Apart from the above major ones, I did some other minor customizations, like:</p>
<ul>
<li>Removed the <strong>Feed</strong> link from the header and added the RSS icon in the footer</li>
<li>Changed from showing 3 posts on the homepage to 5 posts</li>
<li>Changed the default font (might change again, though)</li>
<li>Added icons for publishing date and tags inside individual posts</li>
<li>Edited the layout file to include open graph images</li>
<li>Completely removed the dark mode from the <code>index.css</code> file, etc.</li>
</ul>
<h2>3. Moving blog posts from WordPress</h2>
<p>I was all set to use the <a rel="noreferrer" target="_blank" href="https://github.com/lonekorean/wordpress-export-to-markdown">wordpress-export-to-markdown</a> tool for converting all my posts to Markdown. But then I discovered <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=WuH5QYCdh6w">Zach's new video</a> where he demonstrates moving 100s of posts from WordPress to Markdown by using the command line tool <code>@11ty/import</code>.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">npx @11ty/import wordpress https://deepakness.com/ <span class="token parameter variable">--output</span><span class="token operator">=</span>content/blog <span class="token parameter variable">--assetrefs</span><span class="token operator">=</span>colocate</code></pre>
<p>I just ran the above terminal command, and all my posts were copied as Markdowns into multiple sub-folders with associated images co-located in those folders. Amazing!</p>
<p>But there was an issue!</p>
<p><img alt="Messy frontmatter of imported WordPress posts" decoding="async" src="https://assets.deepakness.com/blog/from-wordpress-to-11ty/frontmatter-of-imported-wordpress-posts.png"></p>
<p>The frontmatters of the imported WordPress posts were messy. I didn't want <code>authors</code> and <code>metadata</code> and only wanted <code>title</code>, <code>date</code>, and <code>tags</code> from it. I asked ChatGPT for a Python script that can clean the frontmatter of all <code>.md</code> files across multiple sub-folders, and I got it working on the very first try. Below is the Python code, if you're interested:</p>
<pre class="language-python" tabindex="0"><code class="language-python"><span class="token keyword">import</span> os
<span class="token keyword">import</span> yaml
<span class="token keyword">import</span> frontmatter
<span class="token keyword">from</span> pathlib <span class="token keyword">import</span> Path
<span class="token keyword">from</span> datetime <span class="token keyword">import</span> datetime
<span class="token keyword">def</span> <span class="token function">clean_frontmatter</span><span class="token punctuation">(</span>file_path<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"\nReading file: </span><span class="token interpolation"><span class="token punctuation">{</span>file_path<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
<span class="token comment"># Read the markdown file</span>
<span class="token keyword">try</span><span class="token punctuation">:</span>
post <span class="token operator">=</span> frontmatter<span class="token punctuation">.</span>load<span class="token punctuation">(</span>file_path<span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Current frontmatter:"</span><span class="token punctuation">,</span> <span class="token builtin">dict</span><span class="token punctuation">(</span>post<span class="token punctuation">.</span>metadata<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token comment"># Extract only needed frontmatter fields</span>
cleaned_metadata <span class="token operator">=</span> <span class="token punctuation">{</span>
<span class="token string">'title'</span><span class="token punctuation">:</span> post<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">'title'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token string">'date'</span><span class="token punctuation">:</span> post<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">'date'</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token string">'tags'</span><span class="token punctuation">:</span> post<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">'tags'</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Cleaned frontmatter:"</span><span class="token punctuation">,</span> cleaned_metadata<span class="token punctuation">)</span>
<span class="token comment"># Create new post with cleaned frontmatter</span>
new_post <span class="token operator">=</span> frontmatter<span class="token punctuation">.</span>Post<span class="token punctuation">(</span>
content<span class="token operator">=</span>post<span class="token punctuation">.</span>content<span class="token punctuation">,</span>
<span class="token operator">**</span>cleaned_metadata
<span class="token punctuation">)</span>
<span class="token comment"># Write back to file</span>
<span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>file_path<span class="token punctuation">,</span> <span class="token string">'w'</span><span class="token punctuation">,</span> encoding<span class="token operator">=</span><span class="token string">'utf-8'</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>
f<span class="token punctuation">.</span>write<span class="token punctuation">(</span>frontmatter<span class="token punctuation">.</span>dumps<span class="token punctuation">(</span>new_post<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"✅ Successfully cleaned frontmatter"</span></span><span class="token punctuation">)</span>
<span class="token keyword">except</span> Exception <span class="token keyword">as</span> e<span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"❌ Error processing file: </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token builtin">str</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">process_markdown_files</span><span class="token punctuation">(</span>directory<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token comment"># Convert to absolute path if it's relative</span>
base_dir <span class="token operator">=</span> Path<span class="token punctuation">(</span>directory<span class="token punctuation">)</span><span class="token punctuation">.</span>resolve<span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Looking for .md files in: </span><span class="token interpolation"><span class="token punctuation">{</span>base_dir<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
<span class="token comment"># Check if directory exists</span>
<span class="token keyword">if</span> <span class="token keyword">not</span> base_dir<span class="token punctuation">.</span>exists<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"❌ Directory not found: </span><span class="token interpolation"><span class="token punctuation">{</span>base_dir<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
<span class="token keyword">return</span>
<span class="token comment"># Count files</span>
md_files <span class="token operator">=</span> <span class="token builtin">list</span><span class="token punctuation">(</span>base_dir<span class="token punctuation">.</span>rglob<span class="token punctuation">(</span><span class="token string">'*.md'</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Found </span><span class="token interpolation"><span class="token punctuation">{</span><span class="token builtin">len</span><span class="token punctuation">(</span>md_files<span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token string"> markdown files"</span></span><span class="token punctuation">)</span>
<span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>md_files<span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"No markdown files found! Please check the directory path."</span><span class="token punctuation">)</span>
<span class="token keyword">return</span>
<span class="token comment"># Process each file</span>
<span class="token keyword">for</span> file_path <span class="token keyword">in</span> md_files<span class="token punctuation">:</span>
clean_frontmatter<span class="token punctuation">(</span>file_path<span class="token punctuation">)</span>
<span class="token keyword">if</span> __name__ <span class="token operator">==</span> <span class="token string">"__main__"</span><span class="token punctuation">:</span>
<span class="token comment"># Use absolute path to content/blog/blog</span>
current_dir <span class="token operator">=</span> Path<span class="token punctuation">(</span>__file__<span class="token punctuation">)</span><span class="token punctuation">.</span>parent
blog_directory <span class="token operator">=</span> current_dir <span class="token operator">/</span> <span class="token string">"content"</span> <span class="token operator">/</span> <span class="token string">"blog"</span> <span class="token operator">/</span> <span class="token string">"blog"</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Starting frontmatter cleanup..."</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Script location: </span><span class="token interpolation"><span class="token punctuation">{</span>current_dir<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Target directory: </span><span class="token interpolation"><span class="token punctuation">{</span>blog_directory<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
process_markdown_files<span class="token punctuation">(</span>blog_directory<span class="token punctuation">)</span></code></pre>
<blockquote>
<p>Also, the import tool had created an additional <code>blog</code> folder inside the <code>content/blog</code> folder. But that wasn't an issue, I manually copied all sub-folders to the correct folder.</p>
</blockquote>
<p>I was surprised that I didn't get even a single error during the cleaning process. AI is getting a lot better at coding these days.</p>
<h2>4. Making the website live</h2>
<p>I pushed the code to a private GitHub repo and deployed the website on Netlify. The process was straightforward because I already know the process, but the first build time took around 4 minutes which was unusually high. Upon investigating, I found that the Eleventy image plugin transforms all images into multiple different formats during the build process and that takes a lot of time. The more number of images you have, it will take more time.</p>
<p>But the problem was, all images were getting transformed on every build which was unnecessary. I came across <a rel="noreferrer" target="_blank" href="https://github.com/11ty/demo-eleventy-img-netlify-cache">this repo</a> while researching, but adding the suggested code in the <code>netlify.toml</code> didn't help me.</p>
<p>Now, the build process is taking approx. 2:50 mins. but it should take even lesser time. When I remove the <code>.avif</code> version and only keep the <code>.webp</code> version, the build process only takes around 30 seconds. It could work, but it'd be better if AVIF version of images are also present.</p>
<p>So... I am still figuring out how to properly set up the cache plugin, and will update this section as I discover more.</p>
<blockquote>
<p>I also had to add some 301 redirects as I deleted a few pages that I didn't want anymore. For this, I used the Netlify <code>_redirects</code> file approach.</p>
</blockquote>
<h2>5. Next steps</h2>
<p>While I am pretty much satisfied with how the entire set up is, I still have a few things planned to do. Such as:</p>
<ul>
<li>Write a Python script that reads all the Markdown files, generates <code>description</code> for each, and then adds it in the frontmatter. I can use OpenAI's <code>gpt-4o-mini</code> model for this, and it shouldn't cost more than a dollar for generating descriptions for all posts I have.</li>
<li>Create a Node.js script that helps me create new posts faster by creating the require sub-folder, the index.md markdown file, and then also creates the frontmatter properties (have successfully <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/create-posts-faster-11ty/">created the script</a>).</li>
</ul>
<p>That's it, for now!</p>
Remote Backup to Hetzner Storage Box via rsync2025-01-06T08:11:43Zhttps://deepakness.com/blog/hetzner-storage-box-backup-via-rsync/Set up remote rsync backups from macOS to Hetzner Storage Box: order, enable SSH, login, and sync your files.<p>So just after I figured out how to <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/ext-hdd-backup/">backup to an external HDD</a> by using rsync, I started researching about ways to backup my computer files on a remote storage. And guess what… I figured out that as well.</p>
<p>Again, I am using my Macbook Air M2 and backing up remotely to a <a rel="noreferrer" target="_blank" href="https://www.hetzner.com/storage/storage-box">1TB Hetzner Storage Box</a>, and obviously you can get a bigger box if you have more data.</p>
<p>So… now let’s go through the steps:</p>
<h2>Step 1: Order a Hetzner Storage Box</h2>
<p>At the very first, order a storage box that fits your needs. I only had a few hundred GBs of data, so I got the 1TB box. You will need to create an account on Hetzner, if you don’t have one already and then just place the order.</p>
<p><img alt="Hetzner Storage Box Confirmation Email" decoding="async" src="https://assets.deepakness.com/blog/hetzner-storage-box-backup-via-rsync/Hetzner-Storage-Box-Confirmati-SnwGSAciUD0E.png"></p>
<p>It takes some time before they activate your request, and it just took 3 minutes in my case. I received a confirmation email as you see above after the box gets activated.</p>
<p>After you receive the email, it’s time to move to the next step.</p>
<h2>Step 2: Get Storage Box details and login via ssh</h2>
<p>First of all, open the <code>robot.hetzner.com/storage</code> URL and open the box shown on the screen and you should see something like the screenshot below. You need to turn on the <strong>SSH support</strong> option.</p>
<p><img alt="Hetzner Storage Box Settings" decoding="async" src="https://assets.deepakness.com/blog/hetzner-storage-box-backup-via-rsync/Hetzner-Storage-Box-Settings-2-wM7ltGjymD9d.png"></p>
<p>Click on the <strong>Reset password</strong> button (see above screenshot) and set a new password to be used later.</p>
<p>And now run the following command in your computer’s terminal. Not to mention, replace <code>uXXXXXX</code> with your actual user name (see above screenshot).</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">ssh</span> <span class="token parameter variable">-p</span> <span class="token number">23</span> [email protected]</code></pre>
<p>It will ask for the password, so provide the password and you should now be logged in. You should now see the following screen.</p>
<p><img alt="SSH Terminal Login to Hetzner Storage Box" decoding="async" src="https://assets.deepakness.com/blog/hetzner-storage-box-backup-via-rsync/SSH-Terminal-Login-to-Hetzner--jZDln1c0Cqag.png"></p>
<p>After logging in, you can run the help command to see the available options. It will be shown like this.</p>
<p><img alt="Terminal Hetzner Storage Box help Command" decoding="async" src="https://assets.deepakness.com/blog/hetzner-storage-box-backup-via-rsync/Terminal-Hetzner-Storage-Box-h-rhbpCxWrHeZD.png"></p>
<p>Now, let’s go to the next step.</p>
<h2>Step 3: Install rsync on your computer</h2>
<p>Although, you can go through their <a rel="noreferrer" target="_blank" href="https://rsync.samba.org/">official website</a>, the easiest way is to install via Homebrew on your Macbook by running the following command.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">brew <span class="token function">install</span> <span class="token function">rsync</span></code></pre>
<p>It will just take a few minutes to get installed and then you can move to the next step.</p>
<h2>Step 4: Decide folders you want to back up</h2>
<p>I back up my entire <code>Users>deepak</code> folder except a few folders like Library, Applications, etc. You can use the <code>--exclude</code> flag to exclude any number of folders. My final command looks something like this:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--progress</span> <span class="token parameter variable">-e</span> <span class="token string">'ssh -p23'</span> <span class="token parameter variable">--recursive</span> <span class="token parameter variable">--exclude</span> <span class="token string">'Library'</span> <span class="token parameter variable">--exclude</span> <span class="token string">'.*'</span> <span class="token parameter variable">--exclude</span> <span class="token string">'Applications'</span> /Users/<span class="token operator"><</span>USERNAME<span class="token operator">></span>/ <span class="token operator"><</span>uXXXXXX<span class="token operator">></span>@<span class="token operator"><</span>uXXXXXX<span class="token operator">></span>.your-storagebox.de:<span class="token operator"><</span>REMOTE_FOLDER<span class="token operator">></span></code></pre>
<p>In the above command, you will have to replace the following placeholders with the actual value:</p>
<ul>
<li><code><USERNAME></code>: Write the user name of your computer</li>
<li><code><uXXXXXX></code>: Replace this with the user name of your Hetzner Storage Box</li>
<li><code><REMOTE_FOLDER></code>: Replace this with the folder name that you want to save your files in on the storage box</li>
</ul>
<p>You can either run this command directly, or set up an <strong>alias</strong> so that you don’t have to run this big command every time you back up.</p>
<h2>Step 5: Create an easy-to-remember alias</h2>
<p>To avoid typing or copy-pasting the huge command every time, let’s set up an alias. First, open the terminal on your computer and type the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">nano</span> ~/.zshrc</code></pre>
<p>Add the below line at the very end of the file with the <strong>alias</strong> you want to keep. For example, if I keep <code>rsync_hetzner</code> then I would write:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">alias</span> <span class="token assign-left variable">rsync_hetzner</span><span class="token operator">=</span><span class="token string">"rsync -avh --progress -e 'ssh -p23' --recursive --exclude 'Library' --exclude '.*' --exclude 'Applications' /Users/<USERNAME>/ <uXXXXXX>@<uXXXXXX>.your-storagebox.de:<REMOTE_FOLDER>"</span></code></pre>
<p>Make sure to replace the variables as explained previously in the step #4. After that, save and exit the nano editor by pressing <code>Ctrl + X</code>, then <code>Y</code>, and then <code>Enter</code>.</p>
<p>Then, reload your shell by running the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">source</span> ~/.zshrc</code></pre>
<p>And you’re done.</p>
<p>From the next time, you just have to open your terminal and run <code>rsync_hetzner</code> the command and the sync process will start automatically.</p>
<h3>Important Notes</h3>
<p>However, every time you run the command, <strong>it will ask for your Hetzner Storage Box password</strong> in the terminal. But if you don’t want to enter the password every time, you can <a rel="noreferrer" target="_blank" href="https://docs.hetzner.com/storage/storage-box/backup-space-ssh-keys/">follow this tutorial</a> to set up SSH keys.</p>
<p>Also, you might need to provide Full Disk Access to your Terminal app on your Macbook so that it can access all files and folders. I have explained this in detail in the <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/ext-hdd-backup/">step #3 of this blog post</a>.</p>
<p>To re-download the backup from the remote storage box to your computer, you will have to run the following command.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--progress</span> <span class="token parameter variable">-e</span> <span class="token string">'ssh -p23'</span> <span class="token parameter variable">--recursive</span> <span class="token operator"><</span>uXXXXXX<span class="token operator">></span>@<span class="token operator"><</span>uXXXXXX<span class="token operator">></span>.your-storagebox.de:<span class="token operator"><</span>REMOTE_FOLDER<span class="token operator">></span> /Users/<span class="token operator"><</span>USERNAME<span class="token operator">></span>/</code></pre>
<p>You just have to reverse the order of local folder and remote folder, and it works. And this time you will obviously have to remove the <code>--exclude</code> flags. You will find <a rel="noreferrer" target="_blank" href="https://docs.hetzner.com/storage/storage-box/access/access-ssh-rsync-borg/">more about this here</a>.</p>
<p>That’s it.</p>
<p>Hope this helps.</p>
Manual Backup to External HDD using rsync2025-01-06T05:14:28Zhttps://deepakness.com/blog/ext-hdd-backup/Fast manual backups to external HDD on Mac using rsync: install via Homebrew, find drive name, choose folders, exclude dirs, run command.<p>Earlier I used FreeFileSync to backup the updated files and folders to an external HDD, but I was looking for a better and quicker solution <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/the-pain-of-wordpress-backups/">for backups</a> and finally found one. I have used <code>rsync</code> in the past for file transfer but never really thought about this use case of using it to backup laptop’s data to an external drive.</p>
<p>I just run the <code>rsynchdd</code> terminal command, and all updated files/folders and backed up within seconds.</p>
<blockquote>
<p>Before I explain my process, currently, I’m on Macbook Air M2 and using the Seagate Expansion 2TB External HDD as the external backup drive.</p>
</blockquote>
<p>Now, let’s go through the steps one-by-one:</p>
<h2>Step 1: Install <code>rsync</code> on your computer</h2>
<p>The easiest way to install <code>rsync</code> on a Mac computer is via Homebrew by running the following command.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">brew <span class="token function">install</span> <span class="token function">rsync</span></code></pre>
<p>But you can visit their <a rel="noreferrer" target="_blank" href="https://rsync.samba.org/">official website</a> to learn more about the installation process.</p>
<h2>Step 2: Get the exact name of your external drive</h2>
<p>You can run the below terminal command to know the exact name, but make sure that your drive is connected to the computer before running the command.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">ls</span> /Volumes</code></pre>
<p>Copy the exact name of the drive and move to the next step.</p>
<h2>Step 3: Decide which folders you want to back up</h2>
<p>I back up the entire <code>Users/deepak</code> folder except a few folders in it that I exclude by using the <code>--exclude</code> flag, so now my final command becomes like this.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">rsync</span> <span class="token parameter variable">-avh</span> <span class="token parameter variable">--progress</span> <span class="token parameter variable">--exclude</span> <span class="token string">'Library'</span> <span class="token parameter variable">--exclude</span> <span class="token string">'.*'</span> <span class="token parameter variable">--exclude</span> <span class="token string">'Applications'</span> /Users/<span class="token operator"><</span>USER_NAME<span class="token operator">></span>/ /Volumes/<span class="token operator"><</span>EXTERNAL_DRIVE<span class="token operator">></span>/<span class="token operator"><</span>FOLDER<span class="token operator">></span></code></pre>
<p>Here, you will have to replace <code><USER_NAME></code> with your computer’s user name, then replace <code><EXTERNAL_DRIVE></code> with the name of your external drive that you get from the step #2, and then replace <code><FOLDER></code> with the name of the folder that you want to keep in the external drive.</p>
<p>Test the above command and see if those files are being copied in the external HDD or not, if it works then we’re ready to move to the next steps.</p>
<p><img alt="Give Full Disk Access to Terminal" decoding="async" src="https://assets.deepakness.com/blog/ext-hdd-backup/Give-Full-Disk-Access-to-Termi-G9SStJ8WdE9s.png"></p>
<p>You may also need to provide your Terminal app the Full Disk Access by going into the Privacy and Security section in the System Settings app, as you can see in the above screenshot.</p>
<h2>Step 4: Create an alias</h2>
<p>Now, trying to type or even copy-paste the command from the step #3 every time is a difficult task. So… we’re going to create an alias.</p>
<p>If you use the zsh (default on newer macOS versions), then type the following terminal command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token function">nano</span> ~/.zshrc</code></pre>
<p>Add the below line at the very end of the file with the <strong>alias</strong> you want to keep. For example, if I keep <code>rsynchdd</code> then I would write:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">alias</span> <span class="token assign-left variable">rsynchdd</span><span class="token operator">=</span><span class="token string">"rsync -avh --progress --exclude 'Library' --exclude '.*' --exclude 'Applications' /Users/<USER_NAME>/ /Volumes/<EXTERNAL_DRIVE>/<FOLDER>"</span></code></pre>
<p>Make sure to replace the variables in the above command as explained in the step #3. After that, save and exit the nano editor by pressing <code>Ctrl + X</code>, then <code>Y</code>, and then <code>Enter</code>.</p>
<p>Reload your shell by running the following command:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash"><span class="token builtin class-name">source</span> ~/.zshrc</code></pre>
<p>And now you’re done.</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">rsynchdd</code></pre>
<p><img alt="Terminal rsync command sync" decoding="async" src="https://assets.deepakness.com/blog/ext-hdd-backup/Terminal-rsync-command-sync-SLChu3Cz0cSy.png"></p>
<p>Whenever you just type the alias name, i.e. <code>rsynchdd</code> in my case, and press Enter, it’s going to sync all the changes immediately. You just have to connect your HDD to your computer before running the command.</p>
<p>Further, if you want to back up files to a remote storage like Hetzner Storage Box via rsync then I have written <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/hetzner-storage-box-backup-via-rsync/">another post about this</a>.</p>
<p>That’s it.</p>
Creating a Timeline of WP vs WP Engine2024-11-02T03:55:36Zhttps://deepakness.com/blog/creating-a-timeline-of-wp-vs-wp-engine/Timeline tracking the WordPress vs WP Engine dispute, built with Astro, Airtable, and Netlify edge functions: WPvsWPE.report<p>Two weeks ago, I was on a 12-hour train journey and got this idea to create a simple timeline of events of the WordPress and WP Engine conflict out of boredom.</p>
<p>Initially, I just thought to organize all the events for myself so that it’s easier to follow. But later, I bought a domain <a rel="noreferrer" target="_blank" href="https://wpvswpe.report/">WPvsWPE.report</a> and did an <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1846844817017581811">announcement tweet on X</a>.</p>
<p>The idea was to create a simple timeline of events which looks good on all devices, so I started looking for a timeline template and found one from <a rel="noreferrer" target="_blank" href="https://cruip.com/3-examples-of-brilliant-vertical-timelines-with-tailwind-css/">Cruip</a> that uses Tailwind. I liked the template and chose Astro.build for the website because I learned about it recently and wanted to give it a try.</p>
<p>The final tech-stack I used is:</p>
<ul>
<li><a rel="noreferrer" target="_blank" href="https://astro.build/">Astro.build</a> for the website</li>
<li>Airtable for storing the events data, and</li>
<li>Netlify for hosting the site</li>
</ul>
<p>Also, it became extra easy to use Netlify’s edge functions for SSR of data from Airtable, as I found a <a rel="noreferrer" target="_blank" href="https://github.com/sarahetter/astro-netlify-edge-starter">really useful GitHub repo</a> for that. It made my job 2-3X easier.</p>
<p><img alt="" decoding="async" src="https://assets.deepakness.com/blog/creating-a-timeline-of-wp-vs-wp-engine/image-2048x1101-4c2YycRmhOq1.png"></p>
<p>Now, whenever I come across any new development in the WordPress (or, rather Matt Mullenweg) vs WP Engine drama, I just add a new row in my Airtable base (see above screenshot), and it automatically starts showing on the website after some time (I have a 10-min caching set up).</p>
<p>Lastly, I loved working with Astro as it is straightforward and will definitely use this again for my future projects.</p>
Fast Internet, But Sites Open Slow?2024-09-30T11:45:51Zhttps://deepakness.com/blog/fast-internet-slow-sites/Fix slow-loading sites on macOS despite fast internet: in Wi-Fi > TCP/IP, set Configure IPv6 to Link-Local Only. Quick step-by-step.<p>If you’re on a blazing fast internet but “some” sites open really slow, most probably, I’m about to <a rel="noreferrer" target="_blank" href="https://tap2solve.com/">solve your problem</a>. In fact, I was having the same issue for a few months and couldn’t find a working solution, until now.</p>
<p><img alt="Solution to some sites opening very slow on macOS despite having a fast internet." decoding="async" src="https://assets.deepakness.com/blog/fast-internet-slow-sites/image-2048x1778-JQGLOvex6wRO.png"></p>
<p>This is straightforward, and you can follow the below instructions to implement this quickly (for macOS):</p>
<ul>
<li>Open the System Settings app, and navigate to the Wi-Fi area</li>
<li>Click on the Details button to the right of the connected Wi-Fi</li>
<li>Go to the TCP/IP section in the popup that opens, and</li>
<li>Change Configure IPv6 value from Automatically to Link-Local Only</li>
</ul>
<p>And you’re done!</p>
<p>I got to know about this from the below comment on a <a rel="noreferrer" target="_blank" href="https://www.reddit.com/r/HomeNetworking/comments/s0uzfd/fast_internet_speeds_but_loading_websites_very/">Reddit post</a> as I was looking for a solution. The comment is originally for Windows and talks about disabling the IPv6 to get rid of the issue.</p>
<p><img alt="Reddit solution to some sites opening very slow" decoding="async" src="https://assets.deepakness.com/blog/fast-internet-slow-sites/image-1-2048x1200-QWw0zocug0u0.png"></p>
<p>So… I searched how can I disable IPv6 on macOS that I am currently using, and got the solution that I have shown in the screenshot. After implementing the solution, I could really feel the difference – sites that used to stuck are now opening without issues.</p>
<p>I got so excited to find a working solution finally that I even <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1840797500011397160">tweeted</a> about it, as you can see below.</p>
<blockquote>
<p>For some time, I had this problem of some websites opening very slow despite having a fast internet.</p>
<p>Now, after months, I finally found a solution that works.</p>
<p>Click on the Details button of the connected Wi-Fi, then on TCP/IP, and change Configure IPv6 value from Automatically… <a rel="noreferrer" target="_blank" href="https://t.co/4CVYiqD8nI">pic.twitter.com/4CVYiqD8nI</a></p>
<p>— DeepakNess (@DeepakNesss) <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1840797500011397160">September 30, 2024</a></p>
</blockquote>
<h2>Why does it work?</h2>
<p>Now, the next question is, why does disabling IPv6 solve the problem, at least for me?</p>
<p>I don’t know, honestly!</p>
<p>But I found an interesting <a rel="noreferrer" target="_blank" href="https://www.reddit.com/r/Windows10/comments/ryy23p/internet_speed_gets_dramatically_faster_after_i/">post on Reddit</a> that explains it like:</p>
<blockquote>
<p>My first theory is while your LAN/local network has IPv6, your ISP may not provide IPv6 globally so you have to relay all traffic from IPv6 to IPv4 with something <a rel="noreferrer" target="_blank" href="https://serverfault.com/questions/596616/how-do-i-reach-ipv4-addresses-from-an-ipv6-only-network">running NAT64</a>.</p>
</blockquote>
<p>But there’s also a slight chance that some things like VPNs might not work after you disable the IPv6. And if that’s the case, you will have to dig some more to find a solution that works for your case.</p>
Experience with 3-day Fasting2024-09-15T23:30:32Zhttps://deepakness.com/blog/3-day-fast/Personal 72-hour water fast: black coffee, walks, light lifting, minimal cravings, steady energy, lots of water; refeed day 4.<p>So… I decided to do a 3-day or 72-hour fasting after watching the video <a rel="noreferrer" target="_blank" href="https://youtu.be/RuOvn4UqznU?si=5fNPKI_ATGjdg99z">Fasting for Survival</a> by Dr. Pradip Jamnadas. It’s not like I wasn’t aware of the benefits of fasting beforehand, but the video definitely gave me a fresh perspective and kind of pushed me to start the fast.</p>
<p><strong>Below, I will briefly explain what exactly I did and how my experience was:</strong></p>
<ul>
<li><strong>Day #1</strong>
<ul>
<li>I wake up, have a cup of black coffee, and start the fast. For the rest of the day, I’m out to movies and stuff with my friends.</li>
<li>Have cravings during the lunchtime, as I had to watch them eat. But it’s okay, I didn’t eat anything – just water, occasionally.</li>
<li>At the dinner time, I don’t feel any cravings at all. It’s surprising.</li>
</ul>
</li>
<li><strong>Day #2</strong>
<ul>
<li>I wake up feeling fresh, take a 4 kilometers walk in the morning. It was lovely weather, so an added benefit.</li>
<li>I’m feeling energetic as always, no laziness at all. I couldn’t believe that I haven’t eaten for over 24 hours now.</li>
<li>At 4pm, I take half a spoon of ORS powder with a cup of water. Didn’t feel the need, but read somewhere that it’s good.</li>
</ul>
</li>
<li><strong>Day #3</strong>
<ul>
<li>I wake up feeling even more energetic. I even hit the gym in the morning, do some light weightlifting and come back.</li>
<li>It was Monday, so I worked on a few projects and also read books. Didn’t have cravings for lunch or dinner at all.</li>
<li>I was functioning as usual, even though I hadn’t eaten for almost 60 hours now.</li>
</ul>
</li>
<li><strong>Day #4</strong>
<ul>
<li>It’s day #4 morning and time to end the fast. In the morning, I have a cup of black coffee and soupy food after a few hours.</li>
<li>For the next meal, I am back to what I eat normally when I’m not fasting.</li>
</ul>
</li>
</ul>
<p>And that’s all.</p>
<p>During the 3-day water fast, I didn’t feel even a bit of weakness or laziness. I was fully energetic and was doing all physical activities as I normally do. Yes, I was drinking at least 3 liters of water every day, though.</p>
<p>Next month, I’m definitely doing it again!</p>
Introducing PutOut – Publish eBooks as Websites2024-08-22T20:44:13Zhttps://deepakness.com/blog/introducing-putout/Announcing PutOut, a free 11ty/Tailwind tool to publish ebooks as websites, deploy to Netlify, Cloudflare Pages, or GitHub Pages.<p>While I’m into <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/how-i-started-writing/">blogging</a> and SEO since 2015, I started experimenting with programmatic SEO in early 2022 and created <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/">UntalkedSEO</a> to share my learnings as blog posts. I was just documenting everything on the site – from non-technical to technical aspects of pSEO.</p>
<p>A month ago, I decided to compile all my important documentations on programmatic SEO into a nice ebook that anyone can access for free. And luckily, once.com announced their free ebook publishing tool <a rel="noreferrer" target="_blank" href="https://once.com/writebook">Writebook</a> a the same time, and I was hooked. I prepared the ebook and initially published by using the tool on Hetzner.</p>
<p><img alt="Initial version of the ebook hosted on Hetzner via Writebook" decoding="async" src="https://assets.deepakness.com/blog/introducing-putout/image-2048x1320-8WcJFfkhOa9i.png"></p>
<p>Initial version of the ebook hosted on Hetzner via Writebook</p>
<p>Writebook is a great tool that allows you to host it on your own server and even collaborate with multiple people in finalizing the ebook. But… I had to keep paying the server fee of ~$5 every month just to host a static ebook that was rarely going to be updated.</p>
<p>So I started looking for an alternative…</p>
<p>And then <strong>the idea of creating <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/putout">PutOut</a> comes into the play</strong>. I, along with my friend <a rel="noreferrer" target="_blank" href="https://rohit.online/">Rohit</a>, started working on the project and it was live within a week. We used the static site generator called <a rel="noreferrer" target="_blank" href="https://11ty.dev/">11ty</a> along with <a rel="noreferrer" target="_blank" href="https://tailwindcss.com/">Tailwind</a> and now, it can be hosted on platforms like Netlify, Cloudflare Pages, or even on GitHub Pages for free.</p>
<p><a rel="noreferrer" target="_blank" href="https://github.com/deepakness/putout"><img alt="PutOut - Publish eBooks as Websites for FREE" decoding="async" src="https://assets.deepakness.com/blog/introducing-putout/PutOut-Publish-eBooks-as-Websi-PsvEgXRbTnbR.png"></a></p>
<p>We have created detailed documentation on how to get started with the tool as well. It’s fairly simple and very customizable and also has multiple theme options.</p>
<blockquote>
<p>You can check the <a rel="noreferrer" target="_blank" href="https://ebook.untalkedseo.com/">programmatic SEO ebook here</a>, it’s free.</p>
</blockquote>
<p>I, being a non-developer, learnt a lot about developing such a tool and certain other things like:</p>
<ul>
<li>became versed with the 11ty SSG</li>
<li>understood the Tailwind CSS properly now</li>
<li>learnt to set up a proper GitHub repo</li>
<li>collaborating with multiple people on a project</li>
<li>a thing or two about the design as well</li>
<li>efficiently leveraging AI for coding, etc.</li>
</ul>
<p>That’s it.</p>
<p>And now that have tasted the blood, I’m not going to stop! 🤩</p>
Overthinking is a Matrix2024-03-15T09:43:30Zhttps://deepakness.com/blog/overthinking-is-a-matrix/Overthinking is a mental matrix—break out by dropping perfection, taking small steps, and leaning on friends for perspective.<p>Ever find yourself thinking too much about something and can’t stop? That’s overthinking. It’s like being stuck in a matrix in your mind, and it happens when you’re not confident about something or have been procrastinating for some time.</p>
<p>Think of overthinking as being trapped in a matrix or a maze. Getting out seems hard, but actually, you can escape this matrix more easily than you think. All it takes is a little nudge to remind yourself to stop worrying and start doing something.</p>
<p>And when you catch yourself overthinking, try to let go of the need for everything to be perfect. Just take a small step towards doing something, anything really. If you’re really stuck, talking to a friend or someone you trust can help a lot. They certainly help you see things differently and get out of the matrix.</p>
<p>So, next time you find yourself going round and round in your head, remember it’s okay to not have all the answers.</p>
<p>Just start small with what you can do right now.</p>
Playing with Local LLMs2024-02-26T21:56:55Zhttps://deepakness.com/blog/playing-with-local-llms/Exploring local LLMs (Llama 2, Mistral) with Ollama: piping results to Google Sheets, open-source code, demo video, and STT experiments.<p>After experimenting with OpenAI, Claude, and Gemini Pro APIs, I am now deep into playing with locally hosted large language models like <code>llama2</code> and <code>mistral</code>. Initially, I thought this wouldn’t be an exciting experiment, but I was wrong… very wrong, actually. For the past few days, I have been using these LLMs on my local machine and trying to see what they can be used best for.</p>
<p>In fact, I was able to get outputs from local LLMs to a Google Sheets file. And I have even recorded a detailed video about it that you can <a rel="noreferrer" target="_blank" href="https://www.youtube.com/watch?v=q6b9MIktSAc">watch here</a>:</p>
<iframe title="Using Local LLMs for Programmatic SEO Data Preparation in Google Sheets" width="500" height="375" src="https://www.youtube.com/embed/q6b9MIktSAc?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<p>I am using LLMs via <a rel="noreferrer" target="_blank" href="https://ollama.com/">Ollama</a> because it’s extremely easy to use and manage multiple famous models at the same time. And have put all the involved code from my experiment into this <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/ollama-py">GitHub repository</a> (not to mention, you can access it for free).</p>
<p>Apart from this, I am still looking for better use cases for these large language models. I will also be experimenting with local speech-to-text transcription models like <a rel="noreferrer" target="_blank" href="https://github.com/Vaibhavs10/insanely-fast-whisper">Insanely Fast Whisper</a>. Am I starting to transcribe podcasts? No, but will definitely experiment with something interesting use case.</p>
<p>Let’s see!</p>
Publishing from Obsidian using 11ty2024-02-16T03:43:07Zhttps://deepakness.com/blog/publishing-from-obsidian-using-11ty/Experiment: publish Obsidian notes with 11ty + PicoCSS. Write in Obsidian, auto front matter, git auto-push, repo + demo included.<p>I have been a <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/obsidian-love/">long-time user of Obsidian</a> for taking notes, mostly for writing my daily journals. And I have been using the tool for several years now, <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/still-obsidian/">it’s fantastic and better</a> than all other note-taking tools I have ever used.</p>
<p>Today, as I was writing my daily journal, I got a thought what if there’s a way to quickly publish all my writings as blog posts? And I quickly started an experiment.</p>
<p>Since I am already familiar with the <a rel="noreferrer" target="_blank" href="https://11ty.dev">11ty (Eleventy)</a> static site generator, I set up a blog by using a minimal CSS framework called <a rel="noreferrer" target="_blank" href="https://picocss.com/">PicoCSS</a>. And as I wanted a clean look, I didn’t use any pre-built themes or starter projects – did everything from scratch. After the project was set up, I opened the same folder as a new vault from my Obsidian desktop app, and it turned out, that I can successfully create new posts from Obsidian now, without the need to open my 11ty code folder.</p>
<p>To enhance the functionalities, I did the following things as well:</p>
<ul>
<li>Set up the default location for new notes so that all notes I take get saved to the same folder. It will be easier to publish later.</li>
<li>Created an Obsidian template that automatically adds all the required front matter in new posts being created.</li>
<li>Installed the <a href="obsidian://show-plugin?id=obsidian-git">git plugin</a> inside Obsidian so that it automatically keeps pushing new changes to GitHub at a specified interval.</li>
</ul>
<p><img alt="git-plugin" decoding="async" src="https://assets.deepakness.com/blog/publishing-from-obsidian-using-11ty/git-plugin-2048x864-YzR8INRaKH3N.png"></p>
<p>I have made the GitHub repo public, in case you want to access the source code that I was showing in the video above. And you can check the sample site as well:</p>
<ul>
<li><a rel="noreferrer" target="_blank" href="https://github.com/deepakness/obsidian-11ty">GitHub repo</a></li>
<li><a rel="noreferrer" target="_blank" href="https://obsidian11ty.netlify.app/">Sample site</a></li>
</ul>
<p>I spent only a few hours working on it, but there can be several further enhancements made to the publishing flow. For example, the way to add internal links and images can be improved.</p>
<p>I hope this gives you at least some idea to experiment further.</p>
<p><strong>Update</strong>: 17 Feb, 2026</p>
<p>I worked on it a bit more and made the following <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/obsidian-11ty">changes to the GitHub repo</a>:</p>
<ol>
<li><strong>Environment-aware site metadata</strong>: moved from static <code>site.json</code> to <code>site.js</code> so <code>SITE_URL</code> can be configured per environment (local/staging/prod).</li>
<li><strong>SEO hardening</strong>: canonical URL, OG/Twitter metadata, and dynamic fallback image/description support were improved in the base layout.</li>
<li><strong>RSS feed added</strong> at <code>/feed.xml</code> for subscribers and feed readers.</li>
<li><strong>Client-side search added</strong> with generated <code>search-index.json</code> + search UI page + JS behavior.</li>
<li><strong>Better post metadata model</strong>: support for optional <code>updated</code> and <code>image</code> fields, with these reflected in post rendering and social tags.</li>
<li><strong>Sitemap now prefers updated date</strong> (<code>updated</code> fallback to <code>date</code>) for better freshness signals.</li>
<li><strong>Automated quality checks added</strong>: frontmatter validation + internal link checking + CI workflow on push/PR.</li>
<li><strong>Small UX upgrades</strong>: Search/About links in nav, RSS and GitHub in footer, and polished styles for metadata/search views.</li>
</ol>
<p>I will keep improving it, but if you have ideas, please let me know.</p>
The Learning Month2024-01-27T23:57:27Zhttps://deepakness.com/blog/the-learning-month/A planned learning month: diving into WordPress to build programmatic SEO listing sites with CPTs/fields; less social, more research.<p>Every once in a while, I spend the entire month consuming a lot of content and learning something new that I haven’t properly explored earlier. Though, I am open to learning new things all the time, these learning months are special – I plan them.</p>
<p>Previously, the learning month was around September-October 2023 when I was diving deep into the world of AI and coming up with <a rel="noreferrer" target="_blank" href="https://invertedstone.com/store/">cool scripts for InvertedStone</a> and also for <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/store/snapcloud/">SnapCloud</a>. And now, February 2024 will be one when I am re-exploring WordPress and how I can build solid listing sites while using the <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/programmatic-seo/">techniques of programmatic SEO</a>.</p>
<blockquote>
<p>Been using WordPress for almost a decade, but haven't ever used it to create a listing site.</p>
<p>But now, I am exploring and learning about Custom Post Types and Custom Fields; and how they can be used to create good-looking programmatic pages.</p>
<p>As of now, I'm just consuming lots of… <a rel="noreferrer" target="_blank" href="https://t.co/2dVbUaqXXU">pic.twitter.com/2dVbUaqXXU</a></p>
<p>— DeepakNess (@DeepakNesss) <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1751229695126077514">January 27, 2024</a></p>
</blockquote>
<p>While I have been using WordPress since 2015, I haven’t built complex websites with complex themes and plugins tech-stack. But now I really want to, and you might be feeling my excitement from the <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1751229695126077514">tweet above</a>.</p>
<p>So… as per the upcoming learning month’s plan, below is what I will be doing overall. And it’s simple:</p>
<ul>
<li>Since I am trying to programmatically create a good-looking listing website on WordPress, I will be going through countless videos and forum posts to find the most suitable solution for my use case. As WordPress has tons of plugins and themes that can do the same thing, I will have to find the one that suits the best for my needs.</li>
<li>I will be comparatively less active on social media (unless I plan and schedule my posts in advance before the month starts). But I will definitely be open to suggestions and keep sharing my learnings – <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss">mostly on X</a> and <a rel="noreferrer" target="_blank" href="https://www.linkedin.com/in/deepakness/">LinkedIn</a>.</li>
<li>I will keep working on the existing important projects that cannot be delayed but will not be starting any new projects during the timeframe.</li>
<li>Apart from this WordPress thing, I will also be planning for the upcoming videos for the <a rel="noreferrer" target="_blank" href="https://www.youtube.com/@UntalkedSEO">UntalkedSEO YouTube channel</a> and if I have some time, I will spend some more time to fulfil my learning to code desire.</li>
<li>And yes, I also uninstall most of the time-killing apps from my phone during this time.</li>
</ul>
<p>That’s it.</p>
<p>Let’s see how it goes. 🤞</p>
How X (Twitter) Helps2024-01-04T00:53:20Zhttps://deepakness.com/blog/how-x-twitter-helps/From passive scroller to programmatic SEO creator: how X (Twitter) helped me share, grow an audience, launch a course, and earn.<p>I did my very first tweet on June 16, 2024, around the same time when I joined my engineering college. To be honest, I don’t remember the reason for joining Twitter at that time, but I did it anyway.</p>
<blockquote>
<p>Hello Twitter! <a rel="noreferrer" target="_blank" href="https://x.com/hashtag/myfirstTweet?src=hash&ref_src=twsrc%5Etfw">#myfirstTweet</a></p>
<p>— DeepakNess (@DeepakNesss) <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/478513165773176832">June 16, 2014</a></p>
</blockquote>
<p>But no, I didn’t tweet anything significant for the next 3–4 years. I was just a passive consumer of mostly politics and some business-related tweets. And <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/why-i-love-twitter/">I used to love</a> using the platform back then.</p>
<p>From 2021, I started tweeting some random content about blogging & SEO and noticed some people seemed to like them. It wasn’t working though, after months I only had <100 followers; I kept doing the same either way.</p>
<p>My tweets kept getting better with time!</p>
<p>In early 2022, I got into programmatic SEO, started experimenting with it, and started sharing my learnings on Twitter. In fact, I created an entire <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/">blog around programmatic SEO</a> as well.</p>
<blockquote>
<p>Introducing "Programmatic SEO OS"!</p>
<p>📅 Launching on Jan 5, 2023.</p>
<p>This is the current draft, but will be adding more info and categorizing it better.</p>
<p>Anything specific that you want me to cover? <a rel="noreferrer" target="_blank" href="https://t.co/zynm9hqGPe">pic.twitter.com/zynm9hqGPe</a></p>
<p>— DeepakNess (@DeepakNesss) <a rel="noreferrer" target="_blank" href="https://x.com/DeepakNesss/status/1604866646723547136">December 19, 2022</a></p>
</blockquote>
<p>At the end of the year 2022, I even launched the best course on programmatic SEO at the time. And made decent money from it as well.</p>
<p>All because of Twitter!</p>
<p>I <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/why-i-love-twitter/">love Twitter</a> more than any other social media platform because it truly feels like a business-focused community where people share their learnings. No other platform matches the level of Twitter.</p>
<p><strong>Twitter helps me:</strong></p>
<ul>
<li>share and validate my ideas</li>
<li>attract like-minded people</li>
<li>learn tips-tricks and productivity</li>
<li>hold myself accountable</li>
<li>aspire to “create” more</li>
<li>connect to nice people</li>
<li>to learn something new</li>
<li>clear my doubts, etc.</li>
</ul>
<p>Over the years, I have had so many random people supporting me for things I do. And it feels sooooo good.</p>
<p>I am not leaving X (Twitter) ever.</p>
My Work Setup2023-12-20T01:00:24Zhttps://deepakness.com/blog/work-setup/My minimalist work setup: MacBook Air M2, 22-inch Samsung monitor, MAONO mic, no mouse, visible wires—and no plans to upgrade soon.<p>While most of the time I prefer working on my laptop screen, sometimes, I do use an external monitor when I am multitasking. And below has been my setup for over 4 years now.</p>
<p><img alt="DeepakNess Work Setup" decoding="async" src="https://assets.deepakness.com/blog/work-setup/deepakness-work-setup-2048x116-pW2f6TX8evME.jpg"></p>
<p>In the above photo, you are looking at…</p>
<ul>
<li>MacBook Air 13 with M2</li>
<li>Samsung 22-inch Monitor</li>
<li>MAONO AU-A04 Microphone, and</li>
<li>Fake plastic plants</li>
</ul>
<p>I love my current setup and don’t feel the need to upgrade it anytime soon.</p>
<blockquote>
<p>Yes, I don’t use a mouse because my work mostly revolves around writing, and not having a mouse doesn’t make a lot of difference. And I’m habituated to this as well.</p>
</blockquote>
<p>And I don’t mind all the visible wires as well.</p>
<p>If I decide to upgrade my setup, I would just be upgrading my laptop and maybe… maybe my external screen. Apart from these, everything else would definitely stay the same.</p>
<p>See what I’m <a rel="noreferrer" target="_blank" href="https://deepakness.com/now/">doing now</a>!</p>
What You Can Do with Programmatic SEO2023-11-22T09:44:04Zhttps://deepakness.com/blog/what-you-can-do-with-programmatic-seo/Learn how to use programmatic SEO beyond bulk posts—create scalable comparison and alternatives pages that rank and convert.<p>Programmatic SEO is a technique to publish multiple dynamic pages at once, but people are often confused about the different things it can be used for.</p>
<p>Hint: pSEO is not just for creating blog posts in bulk.</p>
<p>In this post, I will be explaining different ways you can <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/programmatic-seo/">implement the techniques of programmatic SEO</a> on your website.</p>
<p>Let’s get to it, then…</p>
<h2>1. Comparison pages</h2>
<p>You can use programmatic SEO to create comparison pages to compare two or more entities with each other, and there are several examples of it. The one simple example that I like is what Web3Forms has done comparing all its competitors, as you see in the below screenshot.</p>
<p><img alt="Comparison using pSEO" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/Comparison-using-pSEO-2048x135-DSo3Gcp9BYA3.png"></p>
<p>You can compare entities side-by-side in a tabular format or one after another in bullet point format. Not only that, you do the comparisons as a simple blog post as well. And pSEO provides you with the flexibility to do all that easily.</p>
<blockquote>
<p>If you create content for promoting affiliate products on your website, then creating comparison pages using programmatic SEO can be a winning strategy in the long run.</p>
</blockquote>
<h2>2. Alternatives pages</h2>
<p>I have seen many companies targeting keywords like alternatives of {competitor} where they would present themselves as the best alternative to their competitors. And if your company has, let’s say, 20 competitors, you can <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/page-template-for-pseo/">design a good page template</a> and publish all those 20 pages instantly.</p>
<p><img alt="Alternatives using pSEO 1" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/Alternatives-using-pSEO-1-dEfJRIeHrOKA.png"></p>
<p><img alt="Alternatives using pSEO 2" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/Alternatives-using-pSEO-2-tN1sUlLjDmAO.png"></p>
<p><img alt="Alternatives using pSEO 3" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/Alternatives-using-pSEO-3-hDbnp2uxgxPJ.png"></p>
<p>The best example is Moosend where it’s targeting its biggest competitors and then presenting itself as the best alternative, as you can see in the above images. Moosend is ranking in the top #5 for most of these terms, and you can imagine how much valuable organic traffic it must be getting.</p>
<h2>3. Blog-style pages</h2>
<p>Creating blog-style pages is the most common use of pSEO as it’s very simple and easy to set up. If you’re on WordPress, you can easily use plugins like <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/pseo-in-wordpress/#a-by-using-the-wp-all-import-plugin">WP All Import to build multiple blog-style pages</a> within no time.</p>
<p><img alt="pSEO in Blog Style in WP" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/pSEO-in-Blog-Style-in-WP-jscCdJJGCEem.png"></p>
<p>For readers, these pSEO pages seem like simple, manually written blog posts. But for that, you need to have a really great page template designed. The above screenshot contains an example of a blog-style pSEO page, created in WordPress.</p>
<h2>4. Landing pages</h2>
<p>Creating landing pages using pSEO is a bit difficult, but they can be a super valuable resource if done well. WordPress is not very suitable for creating bulk <a rel="noreferrer" target="_blank" href="https://slixta.com/articles/best-practices-for-creating-effective-landing-pages">landing pages</a>, but some other tools or platforms can be used.</p>
<p><img alt="Landing pages using pSEO" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/Landing-pages-using-pSEO-2048x-8MziNwAwhp8x.png"></p>
<p>Make.com is the best example of programmatic SEO in the style of landing pages, as you can see in the above screenshot. The page is SEO-friendly and has a clear CTA for the users (or, their potential customers).</p>
<h2>5. Tools and calculators</h2>
<p>Yes, you can even build multiple tools and calculators using the techniques of pSEO as well. Just write the HTML and the JavaScript logic inside the Google Sheets itself or in whatever dataset system you are using.</p>
<p><img alt="Calculator using pSEO" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/Calculator-using-pSEO-AdF5T8zcXjQM.png"></p>
<p>TheFractionCalculator is a <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/pseo-examples/">great pSEO example</a> where it’s creating multiple pages with simple calculators and getting more than 80k monthly organic traffic. In fact, this is possible even with WordPress by <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/tag/wp-all-import/">using the WP All Import plugin</a>.</p>
<p><img alt="Tools using pSEO" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/Tools-using-pSEO-2048x865-PAwaPAg2cRmD.png"></p>
<p>On a more advanced level, you can take a look at what VEED has been doing by creating 100s of free tools on the concept of <a rel="noreferrer" target="_blank" href="https://deepakness.com/blog/engineering-as-marketing/">engineering as marketing</a> and attracting millions of relevant monthly visitors.</p>
<h2>6. Data-focused pages</h2>
<p>It requires a complex setup, but if you have a lot of data about something, then you can also create data-focused pages with charts and huge tables by using the technique. If the data doesn’t change very frequently, then it’s a bit easier.</p>
<p><img alt="Charts using pSEO" decoding="async" src="https://assets.deepakness.com/blog/what-you-can-do-with-programmatic-seo/Charts-using-pSEO-Y6vzRmfPFCpP.png"></p>
<p>SteamCharts is a good example of data-focused pages where they have separate pages for tracking each Steam game and data is very frequently updated as well.</p>
<p>That’s it.</p>
<p>I hope you get some ideas about the possibilities and what you can do with programmatic SEO. And I’m sure there are tons of other kinds of pages you can build apart from what I mentioned in the list here.</p>
Bulk Convert MPGA Audio Files to MP32023-10-22T07:36:39Zhttps://deepakness.com/blog/mpga-to-mp3/Python script to bulk convert 1,000+ .mpga files to .mp3 with pydub; includes code, steps, and GitHub repo.<p>I had over 1,000 audio files in the <code>.mpga</code> format and I wanted to convert them all to <code>.mp3</code> format. For this, I wrote a Python script that does the conversion with style.</p>
<p>I put all the code inside <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/mpga-to-mp3">this GitHub repository</a>.</p>
<p><img alt="MPGA to MP3" decoding="async" src="https://assets.deepakness.com/blog/mpga-to-mp3/screenshot-2048x1200-2k9R3X8cCrmX.jpg"></p>
<p>Now, let’s look at how I converted those 1,000+ files.</p>
<p><strong>1. Put all the <code>.mpga</code> files in a folder</strong></p>
<p>Put all your audio files in a folder and open the VS Code or any other code editor you choose.</p>
<p><strong>2. Create app.py in the same folder</strong></p>
<p>Create the app.py file in the same folder and copy-paste the following code into the file.</p>
<pre class="language-python" tabindex="0"><code class="language-python"><span class="token keyword">import</span> os
<span class="token keyword">import</span> shutil
<span class="token keyword">from</span> pydub <span class="token keyword">import</span> AudioSegment
<span class="token keyword">def</span> <span class="token function">convert_mpga_to_mp3</span><span class="token punctuation">(</span>file_path<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">try</span><span class="token punctuation">:</span>
<span class="token comment"># Load the .mpga file</span>
song <span class="token operator">=</span> AudioSegment<span class="token punctuation">.</span>from_file<span class="token punctuation">(</span>file_path<span class="token punctuation">,</span> <span class="token string">"mp3"</span><span class="token punctuation">)</span>
<span class="token comment"># Convert to mp3 and export</span>
new_file_path <span class="token operator">=</span> file_path<span class="token punctuation">.</span>rstrip<span class="token punctuation">(</span><span class="token string">".mpga"</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">".mp3"</span>
song<span class="token punctuation">.</span>export<span class="token punctuation">(</span>new_file_path<span class="token punctuation">,</span> <span class="token builtin">format</span><span class="token operator">=</span><span class="token string">"mp3"</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Converted </span><span class="token interpolation"><span class="token punctuation">{</span>file_path<span class="token punctuation">}</span></span><span class="token string"> to </span><span class="token interpolation"><span class="token punctuation">{</span>new_file_path<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
<span class="token keyword">return</span> new_file_path
<span class="token keyword">except</span> Exception <span class="token keyword">as</span> e<span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Failed to convert </span><span class="token interpolation"><span class="token punctuation">{</span>file_path<span class="token punctuation">}</span></span><span class="token string"> due to error: </span><span class="token interpolation"><span class="token punctuation">{</span>e<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>
<span class="token keyword">return</span> <span class="token boolean">None</span>
<span class="token comment"># Get all mpga files in the current directory</span>
mpga_files <span class="token operator">=</span> <span class="token punctuation">[</span>f <span class="token keyword">for</span> f <span class="token keyword">in</span> os<span class="token punctuation">.</span>listdir<span class="token punctuation">(</span><span class="token string">'.'</span><span class="token punctuation">)</span> <span class="token keyword">if</span> f<span class="token punctuation">.</span>endswith<span class="token punctuation">(</span><span class="token string">'.mpga'</span><span class="token punctuation">)</span><span class="token punctuation">]</span>
<span class="token comment"># Create an 'mp3' directory if it doesn't exist</span>
<span class="token keyword">if</span> <span class="token keyword">not</span> os<span class="token punctuation">.</span>path<span class="token punctuation">.</span>exists<span class="token punctuation">(</span><span class="token string">'mp3'</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
os<span class="token punctuation">.</span>makedirs<span class="token punctuation">(</span><span class="token string">'mp3'</span><span class="token punctuation">)</span>
<span class="token comment"># Convert all mpga files</span>
<span class="token keyword">for</span> file_path <span class="token keyword">in</span> mpga_files<span class="token punctuation">:</span>
new_file_path <span class="token operator">=</span> convert_mpga_to_mp3<span class="token punctuation">(</span>file_path<span class="token punctuation">)</span>
<span class="token keyword">if</span> new_file_path <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token boolean">None</span><span class="token punctuation">:</span>
<span class="token comment"># Move the mp3 file to the 'mp3' directory</span>
<span class="token keyword">try</span><span class="token punctuation">:</span>
shutil<span class="token punctuation">.</span>move<span class="token punctuation">(</span>new_file_path<span class="token punctuation">,</span> <span class="token string">'mp3/'</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Moved </span><span class="token interpolation"><span class="token punctuation">{</span>new_file_path<span class="token punctuation">}</span></span><span class="token string"> to mp3/ directory"</span></span><span class="token punctuation">)</span>
<span class="token keyword">except</span> Exception <span class="token keyword">as</span> e<span class="token punctuation">:</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Failed to move </span><span class="token interpolation"><span class="token punctuation">{</span>new_file_path<span class="token punctuation">}</span></span><span class="token string"> due to error: </span><span class="token interpolation"><span class="token punctuation">{</span>e<span class="token punctuation">}</span></span><span class="token string">"</span></span><span class="token punctuation">)</span></code></pre>
<p><strong>3. Run the Python code</strong></p>
<p>Install the <code>pydub</code> library by running the <code>pip install pydub</code> command in the terminal, and now run the Python code by running the following command in the terminal:</p>
<pre class="language-bash" tabindex="0"><code class="language-bash">python app.py</code></pre>
<p>And as soon as the code runs, converted <code>.mp3</code> files will start getting saved inside a folder <code>/mp3</code> in the main folder.</p>
<blockquote>
<p>Note that you may need to use pip3 and python3 instead of pip and python, depending upon the Python version and setup your computer has.</p>
</blockquote>
<p>That’s it! You can <a rel="noreferrer" target="_blank" href="https://github.com/deepakness/mpga-to-mp3">go through this GitHub repo</a> to learn more.</p>
<p>Hope this helps.</p>
How to Disable Cloudflare APO2023-09-21T21:39:53Zhttps://deepakness.com/blog/how-to-disable-cloudflare-apo/Step-by-step guide to completely disable Cloudflare APO via API when ads break and uninstalling the WP plugin isn’t enough.<p>I had the <a rel="noreferrer" target="_blank" href="https://blog.cloudflare.com/automatic-platform-optimizations-starting-with-wordpress/#the-benefits-of-automatic-platform-optimization">Cloudflare APO</a> (they call it Automatic Platform Optimization) installed on a website for a few months, it was working all right. But then I enabled ads on the site, and they were not being shown due to APO’s powerful caching.</p>
<p>I was advised to disable the Cloudflare APO thing on my site, and it’s mentioned nowhere how to “completely” disable it. Yes, you can directly uninstall the Cloudflare WordPress plugin, but it still keeps working, as it happened to me and to others.</p>
<p><img alt="Cloudflare APO Disabling" decoding="async" src="https://assets.deepakness.com/blog/how-to-disable-cloudflare-apo/image-BCyOrTPYo1rd.png"></p>
<p>After spending 3 days finding solutions, I finally know <strong>how to completely disable Cloudflare APO from your website</strong>. Let me explain, step by step…</p>
<h2>1. Prepare the command</h2>
<p>First, copy-paste the below command to a plaintext file on your computer, and replace all the variables indicated inside <code>{}</code>.</p>
<pre><code>curl -X PATCH "https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/settings/automatic_platform_optimization" -H "X-Auth-Email: {CF_EMAIL}" -H "X-Auth-Key: {CF_GLOBAL_API_KEY}" -H "Content-Type: application/json" --data '{"value":{"enabled":false}}'
</code></pre>
<h3><code>{CF_ZONE_ID}</code></h3>
<p>Replace <code>{CF_ZONE_ID}</code> with the Zone ID of the added website in the command that you copied-pasted above.</p>
<p><img alt="" decoding="async" src="https://assets.deepakness.com/blog/how-to-disable-cloudflare-apo/image-3-efZr3vKdgJza.png"></p>
<p>You can get the Zone ID on the right side when you open a site in Cloudflare, as shown in the above screenshot.</p>
<h3><code>{CF_EMAIL}</code></h3>
<p>Replace <code>{CF_EMAIL}</code> with the email that you have used for creating your Cloudflare account.</p>
<p><img alt="Cloudflare account email" decoding="async" src="https://assets.deepakness.com/blog/how-to-disable-cloudflare-apo/image-4-6NVKLTmEA9X3.png"></p>
<p>If you don’t remember, you can find the email after clicking on the profile icon in the top-right corner, as it appears in the above screenshot.</p>
<h3><code>{CF_GLOBAL_API_KEY}</code></h3>
<p>Now, replace <code>{CF_GLOBAL_API_KEY}</code> with the Global API Key of your Cloudflare account.</p>
<p><img alt="Cloudflare Global API" decoding="async" src="https://assets.deepakness.com/blog/how-to-disable-cloudflare-apo/image-5-2048x520-E3GVN1q2gvyy.png"></p>
<p>You can get the Global API Key from <code>Profile > API Tokens</code> as shown in the screenshot above. After you click on the View button, you may need to enter your account password in order to see the API key.</p>
<blockquote>
<p>Make sure to keep your API key very safe and do not share with anyone else; otherwise, others can get access to your account and make unwanted changes.</p>
</blockquote>
<p>Your command is now ready! Let’s move to the next step.</p>
<h2>2. Run the command</h2>
<p>Open the terminal on your computer, copy-paste the plaintext command in the terminal, and hit enter.</p>
<p>And you should see an output like this in the terminal:</p>
<pre><code>{
"result": {
"id": "automatic_platform_optimization",
"editable": true,
"value": {
"cf": false,
"enabled": false,
"wordpress": false,
"wp_plugin": false,
"hostnames": [
"yourwebsite.com",
"www.yourwebsite.com"
],
"cache_by_device_type": false
},
"modified_on": null
},
"success": true,
"errors": [],
"messages": []
}
</code></pre>
<p>It means the Cloudflare APO has been successfully disabled for your website. And after it gets disabled, you should also clear all cache by clicking on the <strong>Purge Everything</strong> button under <code>Caching > Configuration</code> of your site in Cloudflare.</p>
<p><img alt="Purge Everything Cache on Clouflare" decoding="async" src="https://assets.deepakness.com/blog/how-to-disable-cloudflare-apo/image-7-2048x639-J5CYb3q2QjvP.png"></p>
<p>That’s it.</p>
<p>Hope this helps!</p>
FFmpeg is Wild2023-09-18T19:45:13Zhttps://deepakness.com/blog/ffmpeg-is-wild/Speed up, compress, and add music to videos in seconds with one FFmpeg command—plus a simple script to automate it.<p>I recorded a 1:07 long screen record video to showcase how the <a rel="noreferrer" target="_blank" href="https://untalkedseo.com/store/pseo-keyword-ideas-database/">pSEO keyword ideas database</a> works. While the video could have been directly uploaded to YouTube and Twitter, I wanted to enhance the watchers’ experience a bit by making it a bit faster and adding nice music.</p>
<p>And a single terminal command did all of this; first, watch the video below, and then I’ll show how.</p>
<iframe title="pSEO Keyword Ideas Database | Cool Keyword Ideas for Programmatic SEO" width="500" height="375" src="https://www.youtube.com/embed/0knbN3yqTTE?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<p>And it all took me just a few seconds.</p>
<p>Cool, right?</p>
<h2>Editing videos with FFmpeg</h2>
<p>First, you will need to install the FFmpeg tool for your computer and then start with the command. You can get it from <a rel="noreferrer" target="_blank" href="https://ffmpeg.org/download.html">their official website</a>.</p>
<p>For the raw video, I had the following requirements:</p>
<ul>
<li>Make the video a bit faster</li>
<li>Compress the video for social media, and</li>
<li>Add music at a low volume</li>
</ul>
<p>For this, first, I created a file called <code>fast.sh</code> in the same folder where my raw video <code>your_raw_video.mov</code> and the music <code>audio.mp3</code> were present. I copy-pasted the following command into the executable file and got the final video <code>your_final_video.mp4</code> within seconds (not even minutes).</p>
<pre><code># Extract duration in seconds
duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 your_raw_video.mov)
# Calculate new duration after speed up
new_duration=$(bc -l <<< "$duration*0.75")
# Remap audio and video
ffmpeg -i your_raw_video.mov -i audio.mp3 -vcodec libx264 -crf 28 -filter_complex "[0:v]setpts=0.75*PTS[v];[1:a]volume=0.6,atrim=0:$(printf "%.0f" $new_duration)[a]" -map "[v]" -map "[a]" your_final_video.mov
</code></pre>
<p>Here’s how you can execute the <code>fast.sh</code> file:</p>
<ol>
<li>Run <code>chmod +x fast.sh</code> in the terminal in the same folder, and</li>
<li>Run <code>./fast.sh</code> command in the terminal</li>
</ol>
<p>And this single terminal command did the following thing to the raw video:</p>
<ul>
<li>Made the video 1.33x faster</li>
<li>Compressed the video by 92%, and</li>
<li>Added music at 60% volume</li>
</ul>
<p>The raw screen record video was 1 minute and seconds long and 71.4 MB in file size, and the final video was 50 seconds long and 5.4 MB in file size.</p>
<p>If I had used a video editor tool like iMovie or Adobe Premier Pro, it must have taken me several 5–10 minutes, at least. But FFmpeg did all this in less than 20 seconds.</p>
<p>Now, isn’t that wild?</p>
Never Understood What People Might Want2023-09-04T03:17:56Zhttps://deepakness.com/blog/never-understood-what-people-might-want/I misjudged demand until a simple Google Sheets–OpenAI script sold well. Lesson: stop overthinking; ship, add a buy button, iterate.<p>I have this habit of thinking about what people might be interested in buying and what they would never buy, and I get it wrong all the time. I have rarely anticipated it right.</p>
<p>For example, for more than a year, I have been using a setup of Google Sheets and the OpenAI API to create descriptions and pretty much all different kinds of text content. Over the last few months, several people suggested that I sell the setup to others, but I didn’t consider the idea. And the main reason was that I thought, there are already several extensions and plugins for that, so <strong>why would anyone buy a simple script from me?</strong></p>
<p>And that “<strong>why</strong>” has always deceived me.</p>
<p>After my friend <a rel="noreferrer" target="_blank" href="http://bikashkampo.com/">Bikash</a> also suggested making the setup available for others, I thought of giving it a try. I listed the script on Gumroad, created a <a rel="noreferrer" target="_blank" href="https://invertedstone.com/store/google-sheets-to-openai-api/">landing page</a>, launched it on <a rel="noreferrer" target="_blank" href="https://www.producthunt.com/posts/google-sheets-openai-api">Product Hunt</a>, and then <a rel="noreferrer" target="_blank" href="https://twitter.com/DeepakNesss/status/1698147047332106581">tweeted</a> about it. And to my surprise, tens of people have bought that simple script that I didn’t consider sellable.</p>
<p><strong>What am I doing next?</strong></p>
<p>After failing to see and understand what might people be interested in, I will now try to think less and directly list for sale — I’ll just put another buy button on the internet. And if it works, good, if doesn’t, I will swiftly take it down.</p>
<p>Simple, right?</p>