<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="pretty-atom-feed.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>DeepakNess - Blog</title>
  <subtitle>Posts on what I&#39;m building, learning, and thinking about.</subtitle>
  <link href="https://deepakness.com/feed/feed.xml" rel="self" />
  <link href="https://deepakness.com/" />
  <updated>2026-09-23T09:09:51Z</updated>
  <id>https://deepakness.com/</id>
  <icon>https://deepakness.com/img/deepakness.png</icon>
  <logo>https://deepakness.com/img/deepakness.png</logo>
  <author>
    <name>DeepakNess</name>
  </author><entry>
    <title>Building CogSend: A Self-Hosted Social Media Scheduler</title>
    <link href="https://deepakness.com/blog/cogsend-intro/" />
    <updated>2026-09-23T09:09:51Z</updated>
    <id>https://deepakness.com/blog/cogsend-intro/</id>
    <summary>CogSend is a self-hosted social media scheduler for Mastodon, Bluesky, LinkedIn, Threads and X that runs on your own Cloudflare account.</summary>
    <content type="html">&lt;p&gt;I have been using &lt;a href=&quot;https://go.deepakness.com/typefully&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Typefully&lt;/a&gt; for a long time and it is genuinely great, but it supports a fixed set of platforms and I wanted to decide that for myself. So I built my own. Say hello to &lt;a href=&quot;https://cogsend.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;CogSend&lt;/a&gt;, a self-hosted social media scheduler.&lt;/p&gt;
&lt;p&gt;CogSend is &lt;a href=&quot;https://github.com/cogsend/cogsend&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;open source under the MIT license&lt;/a&gt; and it runs on your own free Cloudflare account. There is no monthly fee and no account with me, and your platform credentials stay in your infrastructure. It currently supports Mastodon, Bluesky, LinkedIn, Threads and X with more platform support coming soon.&lt;/p&gt;
&lt;video controls=&quot;&quot; preload=&quot;none&quot; playsinline=&quot;&quot; data-plyr=&quot;&quot; poster=&quot;https://assets.deepakness.com/blog/cogsend-intro/cogsend-demo-poster.webp&quot;&gt;
  &lt;source src=&quot;https://assets.deepakness.com/blog/cogsend-intro/cogsend-demo.mp4&quot; type=&quot;video/mp4&quot;&gt;
&lt;/video&gt;
&lt;h2&gt;What it can do&lt;/h2&gt;
&lt;p&gt;The main idea is, you write a post once, then customize it per platform if you want. There is a Global tab plus a tab for each platform you override, so the X version can stay short while the LinkedIn version says the same thing at length. If you paste a long draft, it splits into a thread that fits the tightest platform you selected, counted the way that platform counts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Publish now or schedule:&lt;/strong&gt; Every destination reports its own result, so one platform failing does not take the rest down with it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retries take care of themselves:&lt;/strong&gt; Retryable failures back off and try again a few times. If a platform keeps refusing (sometimes, happens with Threads), the post parks under Failed with the reason, and you can fix it and retry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manage it all from Posts:&lt;/strong&gt; Cancel or reschedule a queued post without opening it, or retry only the destinations that failed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Images and links:&lt;/strong&gt; Supports up to four images per post with alt text for each one and preview cards for links.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Insights:&lt;/strong&gt; Published against failed over 7, 30 or 90 days, bucketed in your own timezone, with failures grouped by reason instead of a wall of API errors.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Locked down:&lt;/strong&gt; One admin account with an authenticator app, credentials encrypted at rest, and an API key for scripts, Shortcuts and cron jobs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt=&quot;Compose screenshot from CogSend app&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/cogsend-intro/cogsend-screenshot.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;How to install&lt;/h2&gt;
&lt;p&gt;You need Node and a Cloudflare account with Workers access, D1 and R2 available. And the whole install process is just two commands:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone &lt;span class=&quot;token parameter variable&quot;&gt;--depth&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; https://github.com/cogsend/cogsend.git cogsend
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; cogsend &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run setup&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;setup&lt;/code&gt; does everything – creates the database and the bucket, generates the secrets, applies the migrations, deploys the Worker, and then signs in once against the live site to prove it works. At the end just open the final URL it prints, scan the QR with an authenticator app, save the backup codes, and start connecting your social accounts.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Cloudflare free plan is enough for a personal instance. By the way, R2 does ask for a payment method on file even on the free tier, but the first 10 GB each month are free.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I think, it&#39;s more than enough for a single user, no?&lt;/p&gt;
&lt;h2&gt;Why I built it&lt;/h2&gt;
&lt;p&gt;Every new post and note I published went out through a &lt;a href=&quot;https://deepakness.com/blog/publish-fast/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;small script&lt;/a&gt; to the same two platforms. It worked, but I wanted per-post control over where things go, and room to add the platforms I actually use. That is the part CogSend fixes, and now the accounts and the scheduling are mine to change.&lt;/p&gt;
&lt;p&gt;I already self-host a few other things, like &lt;a href=&quot;https://deepakness.com/blog/pi-website-stack/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this website running from a Raspberry Pi&lt;/a&gt;, so putting the scheduler on infrastructure I control felt natural, and the best thing is I loved the process of building it.&lt;/p&gt;
&lt;h2&gt;What&#39;s next&lt;/h2&gt;
&lt;p&gt;First, I will be fixing the documentation and I want the setup to be clear enough that nobody has to read the repo to understand it. And I will record a few short videos for the parts that are easier to show than to write.&lt;/p&gt;
&lt;p&gt;After that, will focus on adding more platforms – Instagram, TikTok and YouTube are the ones I am thinking about. Along with these, I&#39;m thinking about more integration options as well.&lt;/p&gt;
&lt;p&gt;These are early thoughts though, not the final roadmap.&lt;/p&gt;
&lt;p&gt;The code is on &lt;a href=&quot;https://github.com/cogsend/cogsend&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub&lt;/a&gt; and the site is &lt;a href=&quot;https://cogsend.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;cogsend.com&lt;/a&gt;. It is MIT licensed, so fork it and change whatever you want, and send PR as well.&lt;/p&gt;
&lt;p&gt;That&#39;s it.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Serving a Website from a 1 GB Pi 4B on Home WiFi</title>
    <link href="https://deepakness.com/blog/pi-website-stack/" />
    <updated>2026-09-13T22:00:16Z</updated>
    <id>https://deepakness.com/blog/pi-website-stack/</id>
    <summary>How a 1 GB Raspberry Pi 4B on home WiFi serves pi.deepakness.com, through Caddy and a Cloudflare Tunnel.</summary>
    <content type="html">&lt;p&gt;I have a Raspberry Pi 4B at home on WiFi, and it serves a public website. It is a Rev 1.5 board with 4x Cortex-A72 at 1.8 GHz, 1 GB of RAM, a 64 GB microSD card and Debian 13 (trixie). Nothing is forwarded on my router, no ports are open, and my home IP address is never published.&lt;/p&gt;
&lt;p&gt;The page is live at &lt;a href=&quot;https://pi.deepakness.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pi.deepakness.com&lt;/a&gt;, and it is about the machine serving it. You get live readings for CPU, memory, temperature, disk, network and WiFi signal, an hour of history, an availability record with the state of every service, the home connection measured four times a day, and how many &lt;a href=&quot;https://x.com/DeepakNesss/status/2098992508256788748&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;people are reading right now&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The https://pi.deepakness.com website&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/pi-website-stack/piserver.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The path from the internet to the Pi&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;The Pi sits on my home WiFi as a normal client.&lt;/li&gt;
&lt;li&gt;Caddy serves the site files on &lt;code&gt;127.0.0.1:8080&lt;/code&gt;, which exists only inside the Pi.&lt;/li&gt;
&lt;li&gt;A Cloudflare Tunnel dials out to Cloudflare and holds that connection open.&lt;/li&gt;
&lt;li&gt;Cloudflare publishes the local address as a subdomain of mine, with TLS handled at the edge.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Caddy listens on localhost only, so nothing on my network can reach the site directly, and the tunnel is the only way in. There is no port forwarding on the router, no static IP, and no dynamic DNS record to keep updated.&lt;/p&gt;
&lt;p&gt;I had the tunnel pattern written down earlier when I was looking at &lt;a href=&quot;https://deepakness.com/raw/local-apps-to-internet/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ways to publish local apps&lt;/a&gt;, and the feed reader that runs on this Pi &lt;a href=&quot;https://deepakness.com/blog/freshrss-on-pi/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;uses the same route&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Raspberry Pi via Cloudflare Tunnel&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/pi-website-stack/pi-cloudflare-tunnel.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Getting in without opening SSH&lt;/h3&gt;
&lt;p&gt;Tailscale is the other half of this setup. The Pi is on my tailnet, which is a private network between my own devices, and it is how I open a shell on the Pi or reach anything else at home from anywhere. It runs as a system service and is not in the serving path at all. The website would work without it, and the admin access would work without the tunnel.&lt;/p&gt;
&lt;p&gt;Port 22 is never exposed to the internet, and I still get a terminal on the Pi from basically anywhere.&lt;/p&gt;
&lt;h2&gt;What Caddy does&lt;/h2&gt;
&lt;p&gt;The config is mostly security headers and caching. These are the parts that matter, minus the headers:&lt;/p&gt;
&lt;pre class=&quot;language-caddyfile&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-caddyfile&quot;&gt;:8080 {
	bind 127.0.0.1

	@page path / /index.html
	handle @page {
		header Cache-Control &quot;public, max-age=0, s-maxage=60&quot;
		file_server
	}

	@live path /hits.json /uptime.json /speedtest.json
	handle @live {
		header Cache-Control &quot;public, max-age=0, s-maxage=15&quot;
		file_server
	}

	handle /hit {
		respond 204
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The page is cached at the edge for 60 seconds and the data files for 15, which is just longer than the collector&#39;s sampling interval. The numbers stay seconds behind the Pi without the Pi answering every poll from every open tab, and the page&#39;s own polls still carry &lt;code&gt;no-store&lt;/code&gt;, so those do come here.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/hit&lt;/code&gt; is one request per page load, answered with an empty 204 and never cached. That is how page views are counted. Counting them from the access log stopped working once the page was cached at the edge, since the Pi only saw cache fills and the counter advanced about once per edge location per minute, whatever the size of the audience.&lt;/p&gt;
&lt;p&gt;That caching is what got it through the afternoon it was shared. Four IP addresses and later much more fetched the page in a loop, the combined peak reached &lt;strong&gt;1,028 requests per second&lt;/strong&gt;, and the first version of this setup crashed badly.&lt;/p&gt;
&lt;p&gt;However, the same board then took 91 million requests in around 15 hours, with 85 million stopped at Cloudflare and &lt;strong&gt;2.76 million served by origin&lt;/strong&gt;, and it still stayed up.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Cloudflare analytics dashboard showing 91 million requests&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/pi-website-stack/cf-91m.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;What keeps it running&lt;/h2&gt;
&lt;p&gt;Caddy, the collector and the speed test run as systemd user services, symlinked into &lt;code&gt;~/.config/systemd/user/&lt;/code&gt; from the project. And changes are committed in the repo on the Pi, then the affected unit restarts.&lt;/p&gt;
&lt;p&gt;Each service has a memory cap, because memory is the thing that actually runs out on a 1 GB board:&lt;/p&gt;
&lt;pre class=&quot;language-ini&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-ini&quot;&gt;&lt;span class=&quot;token key attr-name&quot;&gt;MemoryMax&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;256M&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;MemoryHigh&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;192M&lt;/span&gt;
&lt;span class=&quot;token key attr-name&quot;&gt;Environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token value attr-value&quot;&gt;GOMEMLIMIT=192MiB&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, Caddy and cloudflared are both Go, and Go holds on to its heap after a burst instead of giving it back. The &lt;code&gt;GOMEMLIMIT&lt;/code&gt; above is the setting doing the real work, since the runtime then keeps itself under the soft cap instead of borrowing memory the Pi does not have.&lt;/p&gt;
&lt;p&gt;A watchdog covers what I cannot. A two minute timer probes the public URL and reads the tunnel&#39;s memory, and restarts the tunnel if two probes fail or if it crosses 250 MB. It has restarted it a few times, mostly during the attack. A five second restart is cheaper than an outage nobody is watching.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;systemctl --user status output for both units&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/pi-website-stack/pi-services-status.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The collector&lt;/h2&gt;
&lt;p&gt;Everything on the page comes from one Python file with no dependencies outside the standard library. It reads &lt;code&gt;/proc&lt;/code&gt; and &lt;code&gt;/sys&lt;/code&gt; every 10 seconds.&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;meminfo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    out &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; line &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; rfile&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/proc/meminfo&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;splitlines&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rest &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; line&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;partition&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        out&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;split&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; out&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The JSON goes into &lt;code&gt;/dev/shm&lt;/code&gt;, which is RAM, because a file every 10 seconds is over 3 million writes a year and the SD card is the first part of a Pi to fail. The counters are the only thing that touches the card, flushed once a minute with a &lt;code&gt;.bak&lt;/code&gt; beside them, and every write goes through &lt;code&gt;os.replace&lt;/code&gt; so a reader never sees a half-written file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The bug worth reading this post for:&lt;/strong&gt; The collector used to empty the access log once it passed a size cap. Caddy holds that file open and keeps its own position in it, so the next write landed past the new end – a sparse file, enormous in size and nearly empty in content, over the cap again on the next tick, emptied again ten seconds later, forever. Visitors read 1 while the site was fast, the CPU was idle and every service reported active. But now, Caddy rotates the log itself, and the collector only reads it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The counters:&lt;/strong&gt; Page views come from the page&#39;s own &lt;code&gt;/hit&lt;/code&gt; request, and readers are counted per device – an open tab sends a random id it keeps in &lt;code&gt;localStorage&lt;/code&gt; on the JSON polls it already makes, so two devices on one WiFi count as two. What reaches the collector is a salted hash, held in memory for three minutes, with an address as the fallback for a browser that keeps no storage. And nothing is written to disk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Response times and the speed test:&lt;/strong&gt; Response times come from the access log. The speed test runs four times a day against Cloudflare&#39;s edge, since Ookla&#39;s own CLI does not work here, so the script uses &lt;code&gt;curl&lt;/code&gt; with a browser user agent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A weak power supply:&lt;/strong&gt; The collector also reads &lt;code&gt;vcgencmd get_throttled&lt;/code&gt; for undervoltage and throttling flags, so a weak charger shows up on the page.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The Right Now section with uptime, CPU, memory, temperature and more&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/pi-website-stack/pi-page-now.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The live page itself&lt;/h2&gt;
&lt;p&gt;Four files with no framework and no build step. &lt;code&gt;index.html&lt;/code&gt;, &lt;code&gt;style.css&lt;/code&gt;, &lt;code&gt;app.js&lt;/code&gt; and a one line &lt;code&gt;beacon.js&lt;/code&gt; that reports the page load come to about &lt;strong&gt;14 KB gzipped&lt;/strong&gt; in total.&lt;/p&gt;
&lt;p&gt;The icons are one inline SVG sprite with 26 symbols, so a page with this many small labels still makes no extra requests. By the way, I printed the numbers in full with separators, so a visitor count reads 1,247 rather than 1.2k.&lt;/p&gt;
&lt;p&gt;There are no cookies and no third-party requests, and a &lt;code&gt;default-src &#39;self&#39;&lt;/code&gt; policy keeps it that way. The one identifier anywhere is the random device id above, and it never leaves the browser except as a salted hash in memory. For my other sites I use &lt;a href=&quot;https://deepakness.com/blog/self-hosting-umami-analytics/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the analytics setup I run on a VPS&lt;/a&gt;, and this page needs none of that.&lt;/p&gt;
&lt;h2&gt;The WiFi link, measured&lt;/h2&gt;
&lt;p&gt;The Pi is a client on my network rather than a server, so the WiFi link is the slowest part of the setup.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The link runs on 5 GHz, channel 36, at a reported 270 Mbit/s, but the real throughput measures around 43 Mbps against Cloudflare&#39;s edge.&lt;/li&gt;
&lt;li&gt;Upload matters more than download here, because the visitors pull the page from my house, so the upload side of the line is what everyone shares.&lt;/li&gt;
&lt;li&gt;Power save can add latency, and NetworkManager reports it as &lt;code&gt;default&lt;/code&gt; on this setup, and it is the first setting I would check if SSH felt sluggish.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt=&quot;The Pi next to the router&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/pi-website-stack/pi-and-router.jpg&quot;&gt;&lt;/p&gt;
&lt;h2&gt;What it costs to run&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Between 3-5 W of electricity.&lt;/li&gt;
&lt;li&gt;Around 250 MB of the 906 MB of RAM the OS can see, with the web server, the collector and the tunnel running.&lt;/li&gt;
&lt;li&gt;Under 2% CPU while idle, and about a millisecond for Caddy to hand over the page at the median.&lt;/li&gt;
&lt;li&gt;43 Mbps of a 200 Mbps fiber line, which is the WiFi link rather than the board.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the way, could a Zero 2 W do the same job?&lt;/p&gt;
&lt;h2&gt;What I learned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory is what fills first:&lt;/strong&gt; Caddy and cloudflared both kept growing under load, on a board with 906 MB. The caps are what stop that from ending in an outage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caching made the biggest difference:&lt;/strong&gt; The page is answered at the edge now, so the Pi stopped handing the same bytes to every visitor, and the size of the audience stopped mattering as much.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The WiFi link is the slowest part:&lt;/strong&gt; Slower than the board and slower than the line it sits on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The SD card fails first:&lt;/strong&gt; Keeping the writes in RAM is the one habit I would keep from this project.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Counting readers without collecting anything was the hard half:&lt;/strong&gt; Reading &lt;code&gt;/proc&lt;/code&gt; is a solved problem.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The page is live at &lt;a href=&quot;https://pi.deepakness.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pi.deepakness.com&lt;/a&gt;, and it updates every 10 seconds. The Caddyfile, the collector, the units, and the watchdog are &lt;a href=&quot;https://github.com/deepakness/piserver&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;on GitHub&lt;/a&gt; in a public repo.&lt;/p&gt;
&lt;p&gt;Overall, it was a super fun project and I loved every bit of it.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Trying the New DeepSeek Harness</title>
    <link href="https://deepakness.com/blog/deepseek-harness/" />
    <updated>2026-08-16T12:48:37Z</updated>
    <id>https://deepakness.com/blog/deepseek-harness/</id>
    <summary>My experience of installing and setting up the new DeepSeek harness, running it with DeepSeek models via OpenCode Go, and what I liked and disliked about it.</summary>
    <content type="html">&lt;p&gt;After building such great Pro and Flash models, DeepSeek has decided to step into the harness game as well. Their harness is based on this philosophy of &amp;quot;everything is a plugin&amp;quot;, much like the &lt;a href=&quot;https://deepakness.com/blog/pi-agent-setup/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pi coding agent&lt;/a&gt;. I have been using Pi with DeepSeek models for months now, so I was curious to see DeepSeek&#39;s own take on the idea.&lt;/p&gt;
&lt;p&gt;All tech news and influencers are talking about &lt;a href=&quot;https://deepseek.com/harness/en/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the new DeepSeek harness&lt;/a&gt;, so I had to give it a try. And my experience so far is pretty good to be honest. In this post, I will explain how I installed and set up the harness for myself and what I liked and disliked about it.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;DeepSeek harness landing page&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/deepseek-harness/dsh.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;What is the DeepSeek Harness?&lt;/h2&gt;
&lt;p&gt;The DeepSeek Harness or &lt;code&gt;dsh&lt;/code&gt; is an open-source agent harness from DeepSeek AI, and that tagline is not just marketing – every part of the product, from the model adapter to the session log to the agent loop itself, is a plugin. It runs on &lt;a href=&quot;https://github.com/cordiverse/cordis&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cordis&lt;/a&gt;, so there is no core to patch. You swap a part by mounting another plugin beside it.&lt;/p&gt;
&lt;p&gt;The agent can read and edit workspace files, run commands, delegate work to subagents, and keep a plan as it goes.&lt;/p&gt;
&lt;p&gt;It is currently in developer preview, and the team is upfront about it in the repo README. It ships as a Web UI at &lt;code&gt;http://127.0.0.1:3080&lt;/code&gt;, but that is just one way to use it. There is also a headless mode and a Python SDK for programmatic use, though I did not use them this time.&lt;/p&gt;
&lt;h2&gt;Installing the DeepSeek Harness&lt;/h2&gt;
&lt;p&gt;As mentioned on &lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;their GitHub repo page&lt;/a&gt;, using the &lt;code&gt;npx @deepseek-ai/dsh web&lt;/code&gt; command in the terminal is the best way to quickly install and use the harness. But it didn&#39;t work for me and threw the following errors.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;DeepSeek harness installation npx command error&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/deepseek-harness/npx-command-errors.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I then quickly cloned the repo and ran it from the source with the following commands.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/deepseek-ai/deepseek-harness.git
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; deepseek-harness
&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; run build
&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; dsh web&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s straightforward, and the process takes just over a minute. You get this &lt;code&gt;http://127.0.0.1:3080&lt;/code&gt; URL in the terminal that I opened in the browser. It showed me an &amp;quot;internal testing warning&amp;quot; popup and then asked me to provide the official DeepSeek API key or configure later, as you see.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;DeepSeek official API key&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/deepseek-harness/ds-api-key.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Setting up a provider&lt;/h2&gt;
&lt;p&gt;My first impression was that this only works with the official DeepSeek provider, but it works with almost all major providers, and you can add custom ones for any OpenAI-compatible endpoint. Everything lives under Settings &amp;gt; Models.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;DeepSeek harness provider setup&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/deepseek-harness/ds-model-picker.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I added &lt;a href=&quot;https://go.deepakness.com/opencode&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my OpenCode Go API key&lt;/a&gt; as you see above, and everything was set to start using OpenCode models inside the harness.&lt;/p&gt;
&lt;p&gt;I already use DeepSeek v4 Pro and Flash through my OpenCode Go subscription inside Pi, so the harness now runs the same models with the same thinking levels. The key itself lands in &lt;code&gt;~/.dsh/.credentials.yaml&lt;/code&gt;, while the settings file keeps only a reference to it – the UI never shows the secret back. My &lt;code&gt;~/.dsh/settings.yaml&lt;/code&gt; looks like this.&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;llm-pi-ai&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;providers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;opencode-go&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;apiKeyEnv&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; OPENCODE_GO_API_KEY
&lt;span class=&quot;token key atrule&quot;&gt;agent-default-model&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; opencode&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;go
  &lt;span class=&quot;token key atrule&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deepseek&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v4&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;pro
  &lt;span class=&quot;token key atrule&quot;&gt;reasoningEffort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; max&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;llm-pi-ai&lt;/code&gt; key at the top is a fun detail. The generic provider adapter is built on &lt;a href=&quot;https://github.com/earendil-works/pi/tree/main/packages/ai&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pi-ai&lt;/a&gt;, the model-provider library from the same team behind Pi. So the harness and my favorite terminal agent share more than just the plugin philosophy.&lt;/p&gt;
&lt;p&gt;Model changes apply on the next request without restarting the server.&lt;/p&gt;
&lt;h2&gt;Using the DeepSeek Harness&lt;/h2&gt;
&lt;p&gt;After I added a local folder (they call it a workspace), honestly, it felt like I&#39;m using the Codex app or ChatGPT web interface. All sessions are on the left side, and you can easily switch among them across different projects.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Working with the DeepSeek harness&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/deepseek-harness/agents-working.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I really liked the UI and UX.&lt;/p&gt;
&lt;p&gt;The first real task I gave it was a deep read-only audit of this very website&#39;s repo. I asked it to go through every single line of code, from all perspectives, and give me a brief report. It read my &lt;code&gt;AGENTS.md&lt;/code&gt; file first, so all my site rules were in the conversation. Then it split the entire audit into six parallel subagents. One took &lt;code&gt;scripts/&lt;/code&gt;, another &lt;code&gt;_data/&lt;/code&gt;, one went through all 161 blog posts, another all 491 raw notes, and the rest took templates, CSS, and the top-level content pages.&lt;/p&gt;
&lt;p&gt;And here is the part I really liked. Each subagent got its own session, and the dropdown at the top showed me exactly how many were working and the progress on each of them.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Subagent dropdown in DeepSeek harness&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/deepseek-harness/subagents-dropdown.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Apart from the subagents, I also loved the live stats at the bottom – tok/sec, cache hit rate, tool calls, turns, steps. None of it is necessary, but it makes the whole session feel transparent.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Harness useful info&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/deepseek-harness/useful-info.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;There is also a &lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/client/ui-trajectory/README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Trajectory tab&lt;/a&gt; for each session that shows the whole session as a timeline – every turn, step, tool call, and nested sub-tool call, each with its token usage and how long it took. Clicking a request opens its input, output, and timing details. It is the most transparent agent UI I have used.&lt;/p&gt;
&lt;p&gt;It also felt safe. Everything ran inside the workspace, and the UI asked before anything that needed approval.&lt;/p&gt;
&lt;h2&gt;Session modes&lt;/h2&gt;
&lt;p&gt;The harness ships with four session modes, and you pick one when you start a session. A session&#39;s mode is fixed at creation – you cannot switch it mid-session, so a different mode means starting a new session.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;dsh different session modes&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/deepseek-harness/session-modes.png&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Standard mode&lt;/strong&gt; is the full coding agent – file editing, shell, file and web search, skills, planning, goals, subagents, and workflows. It is the default, and the mode I have been using for everything so far.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code mode&lt;/strong&gt; has all of Standard&#39;s capabilities, but the tools are presented through the Code Mode SDK. Instead of one tool call per action, the model writes a single TypeScript program against a generated SDK and runs it in one go – a sequence that would take five round trips becomes one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal mode&lt;/strong&gt; is the opposite extreme, just two tools, a persistent bash shell and the &lt;code&gt;str_replace_editor&lt;/code&gt; file editor. No search, no skills, no subagents, no planning. The smallest possible coding agent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Creator mode&lt;/strong&gt; is for making your own presets. It has all of Standard&#39;s capabilities plus a toolset that can inspect the running harness, experiment with plugins, and author presets. You can duplicate any built-in preset and make it yours, or let the agent draft a custom one for you in Creator mode.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The picker lives in the Agent presets section of the UI, and you can set a default mode for new sessions so you do not have to pick every time.&lt;/p&gt;
&lt;h2&gt;Things I like about the DeepSeek Harness&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Everything is a plugin, for real.&lt;/strong&gt; There is no core to patch, unlike most tools where you get whatever the vendor ships. It feels like Pi&#39;s philosophy with a polished GUI on top, and it is fully open source under MIT – no company decides what features I get.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Web UI is clean.&lt;/strong&gt; Sessions on the left, workspaces, and a Codex, ChatGPT or Gemini-like feel without giving up local files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subagents with live progress.&lt;/strong&gt; The parallel audit would have taken forever in a single thread. Six agents, one dropdown, and live progress on each one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Honest stats at the bottom.&lt;/strong&gt; tok/sec, cache hit rate, tool calls, turns, steps. I wish more tools showed numbers like these.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Any provider you want.&lt;/strong&gt; I plugged in &lt;a href=&quot;https://go.deepakness.com/opencode&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenCode Go&lt;/a&gt; and was done. Keys stay in &lt;code&gt;~/.dsh/&lt;/code&gt; on my machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safe by default.&lt;/strong&gt; A workspace-only sandbox plus approval prompts before risky operations.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Things I didn&#39;t like&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;npx&lt;/code&gt; install did not work for me and I had to run it from source. Not a big deal, but the &amp;quot;one command&amp;quot; pitch should work.&lt;/li&gt;
&lt;li&gt;Starting it is not instant. I have to launch the server from a terminal and then open the localhost URL in the browser before I can start working. It is fine once it is running, but it is more friction than just typing &lt;code&gt;pi&lt;/code&gt; in the terminal.&lt;/li&gt;
&lt;li&gt;It does not ping you when a task finishes. No sound, no notification – the harness just sits quietly in its browser tab, so for long-running tasks I keep switching back to check whether the agent is done yet.&lt;/li&gt;
&lt;li&gt;DeepSeek models are text-only, so pasting an image into the harness does not work. In Pi I use a vision-handoff package that describes images with a separate vision model, and I have not found the equivalent setup in the harness yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;DeepSeek Harness is the GUI-shaped plugin agent I did not know I wanted. Pi stays my terminal agent for side projects and experiments, and Grok Build and Claude Code keep their roles in my &lt;a href=&quot;https://deepakness.com/ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AI stack&lt;/a&gt; – the harness is a strong addition, not a replacement yet.&lt;/p&gt;
&lt;p&gt;After Pi and &lt;a href=&quot;https://deepakness.com/blog/omp-setup/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OMP&lt;/a&gt;, the plugin-first idea has clearly spread, and now DeepSeek ships their own take with real polish. I will keep using it for this site&#39;s work and update this post as I learn more.&lt;/p&gt;
&lt;p&gt;If something breaks, &lt;a href=&quot;https://github.com/deepseek-ai/deepseek-harness/discussions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub Discussions&lt;/a&gt; and &lt;a href=&quot;https://discord.gg/Ycq5dCaS4&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;their Discord server&lt;/a&gt; are the places to report it.&lt;/p&gt;
</content>
  </entry><entry>
    <title>How I Publish Blog Posts and Notes So Fast</title>
    <link href="https://deepakness.com/blog/publish-fast/" />
    <updated>2026-08-03T18:09:05Z</updated>
    <id>https://deepakness.com/blog/publish-fast/</id>
    <summary>A look at the workflow, terminal scripts, VS Code extensions, and simple automation that help me publish blog posts and raw notes fast.</summary>
    <content type="html">&lt;p&gt;A reader recently sent me a message asking:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;How are you able to publish blog posts and raw notes so fast? What&#39;s your setup like, and are you using some background automation or API?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It&#39;s a great question. When people see someone posting quick notes and blog posts regularly, they usually think there&#39;s some complex CMS or API running in the background.&lt;/p&gt;
&lt;p&gt;The truth is, I don&#39;t use any web dashboards or complex APIs. I just have a simple setup with terminal scripts, a few VS Code extensions, and small background automations that remove all friction.&lt;/p&gt;
&lt;p&gt;Here&#39;s how my setup works and why it&#39;s so fast.&lt;/p&gt;
&lt;h2&gt;1. Two types of content: Blog vs Raw notes&lt;/h2&gt;
&lt;p&gt;One of the biggest reasons people take so long to publish is mental friction. If every post needs to be a 2,000-word polished essay, you overthink and end up publishing nothing.&lt;/p&gt;
&lt;p&gt;To solve this, I split my content into two types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Blog posts under &lt;code&gt;/blog/&lt;/code&gt;:&lt;/strong&gt; Longer, detailed guides and essays, like my post on &lt;a href=&quot;https://deepakness.com/blog/omp-setup/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;setting up OMP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Raw notes under &lt;code&gt;/raw/&lt;/code&gt;:&lt;/strong&gt; Short technical notes, quick fixes, links, or random thoughts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Having a &lt;a href=&quot;https://deepakness.com/raw/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;separate section for raw notes&lt;/a&gt; changed everything for me. I don&#39;t worry about perfect structure or length. If I learn a quick trick or fix a bug, I just drop it in a note and publish immediately.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;By the way, I was inspired by &lt;a href=&quot;https://simonwillison.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Simon Willison&#39;s blog&lt;/a&gt; a lot, when I migrated my blog &lt;a href=&quot;https://deepakness.com/blog/from-wordpress-to-11ty/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;from WordPress to 11ty&lt;/a&gt; last year.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. Terminal shortcuts over web dashboards&lt;/h2&gt;
&lt;p&gt;I don&#39;t log into WordPress, Ghost, or any admin panel or CMS to write. Everything lives in plain Markdown files on my computer.&lt;/p&gt;
&lt;p&gt;Instead of opening a folder, creating a file, and manually adding dates and tags, I wrote a few small Node.js scripts.&lt;/p&gt;
&lt;p&gt;When I want to write a new note, I just open my terminal and run:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; raw my-quick-note&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or for a blog post:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; blog my-new-post&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This single command:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creates the Markdown file in the right folder&lt;/li&gt;
&lt;li&gt;Fills in the date, title, and initial tags automatically&lt;/li&gt;
&lt;li&gt;Opens the file directly in VS Code&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It takes less than a second to get from an idea to a ready-to-type Markdown file.&lt;/p&gt;
&lt;h2&gt;3. VS Code extensions that speed up writing&lt;/h2&gt;
&lt;p&gt;I write and edit everything inside VS Code. To keep the writing process smooth and fast, I rely on a few simple extensions as well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ltex-plus.vscode-ltex-plus&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LTeX+&lt;/a&gt;&lt;/strong&gt; (grammar/spell checking using LanguageTool): Great for basic spelling and grammar checks while typing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Markdown All in One&lt;/a&gt;:&lt;/strong&gt; Makes writing Markdown much faster with simple keyboard shortcuts, like &lt;code&gt;Cmd+B&lt;/code&gt; for bold, &lt;code&gt;Cmd+I&lt;/code&gt; for italics, auto-formatting lists, and quick heading shortcuts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=TakumiI.markdowntable&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Markdown Table&lt;/a&gt;:&lt;/strong&gt; Takes the headache out of creating and formatting Markdown tables so everything stays neatly aligned automatically.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. Automation doing the heavy lifting&lt;/h2&gt;
&lt;p&gt;Once I finish writing, I don&#39;t want to spend time optimizing images, making social preview cards, or running multiple Git commands.&lt;/p&gt;
&lt;p&gt;I built a simple command &lt;code&gt;pnpm ready&lt;/code&gt; that runs automatically before publishing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Image optimization:&lt;/strong&gt; It automatically resizes and optimizes all new images so the page loads fast.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OG preview images:&lt;/strong&gt; It generates custom social share cards automatically. I also wrote about how I built &lt;a href=&quot;https://deepakness.com/blog/custom-og-images/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;custom OG images&lt;/a&gt; earlier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asset sync:&lt;/strong&gt; It uploads images to Cloudflare R2 so they load fast, and the site repository stays lightweight.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And to push the post live, I run just one command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;pnpm&lt;/span&gt; raw:push&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs the checks, optimizes images, uploads them to R2, commits the changes, and pushes to GitHub. Netlify picks it up and deploys the site live in around 30 seconds.&lt;/p&gt;
&lt;h2&gt;5. The 2-minute workflow&lt;/h2&gt;
&lt;p&gt;Here&#39;s what publishing a quick note looks like from start to finish:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open terminal and run &lt;code&gt;pnpm raw &amp;lt;title&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Write the note in VS Code&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;pnpm raw:push&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt=&quot;Publishing posts fast via VS Code&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/publish-fast/vs-code-publishing-process.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;That&#39;s literally it. From an idea in my head to a live link on the internet, it takes under two minutes.&lt;/p&gt;
&lt;p&gt;But yes, publishing &lt;a href=&quot;https://deepakness.com/blog/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;blog posts&lt;/a&gt; takes much longer than this as they are more thorough.&lt;/p&gt;
&lt;h2&gt;Why speed matters&lt;/h2&gt;
&lt;p&gt;If publishing a quick thought takes 15 minutes of logging in, clicking buttons, and formatting layout, you will eventually stop doing it. By building small terminal shortcuts and letting scripts handle the boring parts, publishing becomes as fast and natural as writing a note to yourself.&lt;/p&gt;
&lt;p&gt;If you&#39;re curious about the full tech stack and infrastructure powering this site, &lt;a href=&quot;https://deepakness.com/colophon/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;check out the colophon&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Setting Up and Testing OMP (Oh My Pi) Agent</title>
    <link href="https://deepakness.com/blog/omp-setup/" />
    <updated>2026-08-12T00:00:00Z</updated>
    <published>2026-08-02T05:53:23Z</published>
    <id>https://deepakness.com/blog/omp-setup/</id>
    <summary>My OMP (Oh My Pi) setup with DeepSeek v4 Flash as the default driver, Grok 4.5 for vision and planning, and Nemotron 3 Ultra as the secondary model.</summary>
    <content type="html">&lt;p&gt;I wrote about &lt;a href=&quot;https://deepakness.com/blog/pi-agent-setup/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;setting up Pi&lt;/a&gt; a couple of months ago. I still use Pi, but less than before – my daily driver these days is &lt;a href=&quot;https://deepakness.com/raw/supergrok-3-months/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Grok Build&lt;/a&gt;, as noted on my &lt;a href=&quot;https://deepakness.com/ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AI stack page&lt;/a&gt;. I recently discovered &lt;a href=&quot;https://omp.sh/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OMP (Oh My Pi)&lt;/a&gt; – a different terminal coding agent based on Pi – and I have been liking how it works so far. I am still testing it, but one thing I really like is how it lets my model services work together instead of competing.&lt;/p&gt;
&lt;p&gt;DeepSeek v4 Flash does the everyday work, and Grok 4.5 handles vision and planning. Nemotron 3 Ultra is my secondary model, waiting in the fallback chain so a rate limit never stops a session.&lt;/p&gt;
&lt;p&gt;Here&#39;s how it works.&lt;/p&gt;
&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;I use two model services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DeepSeek v4 Flash&lt;/strong&gt; – my default driver. Fast, cheap, and it handles most of what I do day to day.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grok 4.5 (xAI)&lt;/strong&gt; – handles vision and planning, since DeepSeek is text-only.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem was never &amp;quot;which model is best&amp;quot;. The real problems were using all of them without manually switching models every few minutes, and keeping long sessions alive when rate limits hit.&lt;/p&gt;
&lt;h2&gt;Model roles&lt;/h2&gt;
&lt;p&gt;OMP has a &lt;a href=&quot;https://omp.sh/docs/roles&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;model roles feature&lt;/a&gt; where instead of configuring one model, you configure a model for each role in the config file:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The main model that does the actual work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;plan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Used when a task needs planning first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The heaviest model, for the hardest problems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;task&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subagents – the model used when work is delegated to parallel agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;smol&lt;/code&gt; / &lt;code&gt;tiny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lightweight tasks like titles and commit messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vision&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Image analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;designer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UI and design work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;advisor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A second model that watches every turn and can interrupt with concerns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Each role also takes a thinking level: &lt;code&gt;minimal&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;xhigh&lt;/code&gt;, or &lt;code&gt;max&lt;/code&gt;. So &amp;quot;use DeepSeek for everything, but with different thinking effort&amp;quot; is literally a few lines of config. The complete list of roles lives in the &lt;a href=&quot;https://omp.sh/docs/settings&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;settings documentation&lt;/a&gt; under &lt;code&gt;modelRoles&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;My current setup&lt;/h2&gt;
&lt;p&gt;My global config at &lt;code&gt;~/.omp/agent/config.yml&lt;/code&gt; maps roles to models:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;modelRoles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; deepseek/deepseek&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v4&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;flash&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;max
  &lt;span class=&quot;token key atrule&quot;&gt;plan&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;    xai&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;oauth/grok&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;4.5&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;high
  &lt;span class=&quot;token key atrule&quot;&gt;vision&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;  xai&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;oauth/grok&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;4.5&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;medium
  &lt;span class=&quot;token key atrule&quot;&gt;tiny&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;    deepseek/deepseek&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;v4&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;flash&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;high
&lt;span class=&quot;token key atrule&quot;&gt;defaultThinkingLevel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; max&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DeepSeek v4 Flash is the default because it handles most of my work fast and cheap, with &lt;code&gt;max&lt;/code&gt; thinking. Grok 4.5 handles planning and looks at images, since DeepSeek is text-only. And lightweight one-off calls like titles also go to Flash.&lt;/p&gt;
&lt;p&gt;I started with separate profile files pinned per project – a shell function that copied a profile from &lt;code&gt;~/.omp/profiles/&lt;/code&gt; into each project&#39;s &lt;code&gt;.omp/config.yml&lt;/code&gt;. It worked, but I settled on a single role-based config instead. The roles cover the switching I actually need, and one global config is easier to maintain than a profile per project.&lt;/p&gt;
&lt;h2&gt;The advisor&lt;/h2&gt;
&lt;p&gt;OMP also has an &lt;a href=&quot;https://github.com/can1357/oh-my-pi/blob/main/docs/advisor-watchdog.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;advisor role&lt;/a&gt; – a second model that reviews each completed turn and can interrupt with a concern if the main model is about to do something wrong. I used exactly that when I started, with a stronger model supervising a cheaper driver.&lt;/p&gt;
&lt;p&gt;My current config has it off:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;advisor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;syncBacklog&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;off&quot;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;immuneTurns&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s a single toggle, so it&#39;s easy to bring back when I want a second set of eyes on a session.&lt;/p&gt;
&lt;h2&gt;Approval mode&lt;/h2&gt;
&lt;p&gt;I run everything in &lt;code&gt;yolo&lt;/code&gt; mode – all commands and edits are auto-approved, no permission popups. I know most people would hate this, but it works for me for two reasons: I review every diff before committing, and the sessions are easy to redo if something goes wrong. The approval modes are worth reading before you decide.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Approval policy is a global setting – role configs only set models, so safety policy stays the same everywhere.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Fallback chains&lt;/h2&gt;
&lt;p&gt;Rate limits come with the models I use most, and I have hit them during long sessions – the session would have stopped dead if OMP didn&#39;t handle it. So Nemotron 3 Ultra sits in a fallback chain as my secondary model, ready when the primary fails:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;retry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;fallbackChains&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; opencode&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;zen/nemotron&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;3&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;ultra&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;free&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the active model fails or exhausts its limits, OMP automatically retries and then moves down the chain instead of stopping. The session keeps going, and once the primary model is available again, it switches back. I have had long sessions survive rate limits this way without touching anything.&lt;/p&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;OMP is still in its testing phase for me. Pi still has its place, mainly for side projects, and I have been using it regularly. So far, here is what I like about how OMP works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DeepSeek v4 Flash as the default driver with &lt;code&gt;max&lt;/code&gt; thinking&lt;/li&gt;
&lt;li&gt;Grok 4.5 for vision and planning&lt;/li&gt;
&lt;li&gt;Nemotron 3 Ultra as the secondary model in the fallback chain&lt;/li&gt;
&lt;li&gt;Fallback chains so a rate limit never stops a session&lt;/li&gt;
&lt;li&gt;YOLO approvals, with diff review before commits&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The best part is that this is all config files – no product decision locked me in. If DeepSeek, Codex, or any other provider releases something better, I can switch to it with one line in the config. If I want a project to behave differently, I can change a role or pin a different config.&lt;/p&gt;
&lt;p&gt;I will keep updating this post as the setup changes.&lt;/p&gt;
</content>
  </entry><entry>
    <title>I Bought a $100 Laptop – Mid 2014 MacBook Pro</title>
    <link href="https://deepakness.com/blog/mbp-mid-2014/" />
    <updated>2026-06-21T00:49:52Z</updated>
    <id>https://deepakness.com/blog/mbp-mid-2014/</id>
    <summary>Saw an ad and immediately bought an old MacBook Pro (Retina, 15-inch, Mid 2014), and I&#39;m super happy about it.</summary>
    <content type="html">&lt;p&gt;Two days ago, I was at home and ordered food online. I went to pre-approve the delivery on the apartment&#39;s app, and saw an ad selling an old MacBook Pro (Retina, 15-inch, Mid 2014, 16 GB RAM, 120 GB SSD) for only ₹12,000 (~$130) from another resident. I inquired immediately and went there to take a look at it.&lt;/p&gt;
&lt;p&gt;The laptop seemed in good condition, I used it for a bit, checked random stuff, and then was ready to buy. I was ready to pay the listed price, but then just asked if they could lower the price to ₹9,000 (~$100) and they agreed immediately. I, then, paid the price and took this 12-year-old laptop home, with its original charger.&lt;/p&gt;
&lt;p&gt;When I asked, their main reason for selling was that they weren&#39;t able to install newer apps – the laptop was still running macOS Big Sur, the last official update from Apple. For me, before my current MacBook Air M2, I had never used any Apple product, so I was just curious about this decade-old laptop.&lt;/p&gt;
&lt;p&gt;By the way, this is how the laptop looks. I absolutely love the backlit Apple logo here.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Apple MacBook Pro Mid-2014, Lit Apple Logo&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/mbp-mid-2014/backlit-logo.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;After getting the laptop, I researched a bit about it and used the &lt;a href=&quot;https://dortania.github.io/OpenCore-Legacy-Patcher/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenCore Legacy Patcher (OCLP)&lt;/a&gt; and upgraded to macOS Ventura 13.7.8 and it&#39;s running fine so far. I tried installing the Codex app and a few other apps on it and they all work as expected, no lag or slowness at all. Obviously, it&#39;s not as powerful as my M2 Air, but it&#39;s more than worth the price I paid.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;MacOS Ventura on Mid 2014 MacBook Pro via OpenCore&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/mbp-mid-2014/macos-ventura.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I haven&#39;t spent much time on it yet, but it seems like a good machine. It&#39;s heavy, but I can keep it open on my desk and use it for scraping or some long-running tasks, so I don&#39;t have to keep my main laptop running (Codex /goal, I&#39;m looking at you).&lt;/p&gt;
&lt;p&gt;I even tried installing &lt;a href=&quot;https://omarchy.deepakness.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Omarchy Linux&lt;/a&gt; on an external SSD connected to the laptop, and it works fine as expected. Here&#39;s a picture of it booted into the &lt;a href=&quot;https://omarchy.org&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Omarchy OS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Omarchy OS on MacBook Pro from Mid 2014&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/mbp-mid-2014/omarchy.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;The speakers aren&#39;t great, but that&#39;s not an issue because that&#39;s not what I got it for.&lt;/p&gt;
&lt;p&gt;For now, I will keep exploring things I can do with this second laptop.&lt;/p&gt;
&lt;p&gt;Let&#39;s see how it goes.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Setting Up and Using the Pi Coding Agent</title>
    <link href="https://deepakness.com/blog/pi-agent-setup/" />
    <updated>2026-08-14T00:00:00Z</updated>
    <published>2026-05-29T17:04:47Z</published>
    <id>https://deepakness.com/blog/pi-agent-setup/</id>
    <summary>My experience of setting up the Pi coding agent with DeepSeek models, installing packages, and making it my daily driver.</summary>
    <content type="html">&lt;p&gt;I have been using AI coding tools for a while now. I have used tools like Cursor and OpenAI Codex for main project work, but my current setup is different. Pi paired with DeepSeek is my daily driver, I use Grok Build for some complex tasks, and Claude Code mainly for frontend design work.&lt;/p&gt;
&lt;p&gt;But a few months ago, in May, I came across &lt;a href=&quot;https://pi.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Pi&lt;/a&gt; – an open-source terminal coding agent. It has a small core and lets you add features through extensions and packages, instead of packing everything in by default. I liked the idea, so I decided to set it up and see where it fits.&lt;/p&gt;
&lt;p&gt;I am still figuring out the right balance, but Pi has already become my daily driver. In this post, I&#39;ll share how I installed it, what my setup looks like, and my honest experience so far.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;My Pi coding agent setup&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/pi-agent-setup/my-setup.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;What is Pi?&lt;/h2&gt;
&lt;p&gt;Pi is an open-source terminal coding agent created by &lt;a href=&quot;https://mariozechner.at&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Mario Zechner&lt;/a&gt;. It works in your terminal and gives the AI model four tools by default – &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, and &lt;code&gt;bash&lt;/code&gt; – to work with your codebase.&lt;/p&gt;
&lt;p&gt;There is no built-in plan mode, no sub-agents, no MCP, and no permission popups. Instead, you can add these things through extensions, skills, and packages. This keeps the core small and lets you build exactly the setup you want.&lt;/p&gt;
&lt;p&gt;Pi works with many AI providers – Anthropic, OpenAI, DeepSeek, Google Gemini, Groq, xAI, OpenRouter, Kimi, and basically almost every other provider as well. You can use an API key or log in with an existing subscription.&lt;/p&gt;
&lt;h2&gt;How I use Pi alongside other tools&lt;/h2&gt;
&lt;p&gt;Right now, I use different tools for different kinds of work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pi&lt;/strong&gt; with DeepSeek models as my daily driver for most of my work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grok Build&lt;/strong&gt; for some complex tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; mainly for frontend design and UI work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have also used &lt;strong&gt;Cursor&lt;/strong&gt; and &lt;strong&gt;OpenAI Codex&lt;/strong&gt; for main project work. They are still part of my AI coding journey, even though they are not the tools I currently use most.&lt;/p&gt;
&lt;p&gt;Pi feels right as my daily driver because it&#39;s fast, lightweight, and I can use cheaper models without burning through expensive subscriptions.&lt;/p&gt;
&lt;p&gt;One example: I recently used Pi with the DeepSeek v4 Flash model to scrape 285K URLs from a website. It ran for about 1.5 hours and cost me just $1 through the official DeepSeek API. I used the &lt;code&gt;pi-codex-goal&lt;/code&gt; extension to track the long-running task and it worked without issues. You can &lt;a href=&quot;https://x.com/DeepakNesss/status/2059825248762224779&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;read more about this on X&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Pi scraping task with pi-codex-goal&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/pi-agent-setup/scraping-using-pi.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Installing Pi&lt;/h2&gt;
&lt;p&gt;Installation is one command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://pi.dev/install.sh &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sh&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once installed, start it by typing &lt;code&gt;pi&lt;/code&gt; in your terminal. But first, you need to set up a provider.&lt;/p&gt;
&lt;h2&gt;Setting up OpenCode Go as the provider&lt;/h2&gt;
&lt;p&gt;Pi has a built-in &lt;code&gt;/login&lt;/code&gt; command that makes setting up any provider easy. I am subscribed to &lt;a href=&quot;https://go.deepakness.com/opencode&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OpenCode Go&lt;/a&gt;, so I logged in with my subscription instead of an API key. Here is what I did:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Opened Pi by typing &lt;code&gt;pi&lt;/code&gt; in the terminal&lt;/li&gt;
&lt;li&gt;Typed &lt;code&gt;/login&lt;/code&gt; and chose &lt;strong&gt;OpenCode Go&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Logged in with my existing subscription&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&#39;s it. After logging in, use &lt;code&gt;/model&lt;/code&gt; to pick your model – I chose &lt;code&gt;deepseek-v4-pro&lt;/code&gt; with &lt;code&gt;max&lt;/code&gt; thinking.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Model selector in Pi&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/pi-agent-setup/model-selector.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Earlier I used DeepSeek through the official DeepSeek API on a pay-as-you-go basis, and I still keep that key around as a backup. OpenCode Go now covers most of my usage.&lt;/p&gt;
&lt;p&gt;I default to &lt;code&gt;max&lt;/code&gt; thinking because I want the model to think deeply before making changes. For lighter tasks like the URL scraping, I switch to the faster and even cheaper &lt;code&gt;deepseek-v4-flash&lt;/code&gt; model.&lt;/p&gt;
&lt;p&gt;You can open the model picker with &lt;code&gt;Ctrl+L&lt;/code&gt;, cycle through your scoped models with &lt;code&gt;Ctrl+P&lt;/code&gt;, and change the thinking level with &lt;code&gt;Shift+Tab&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I also use Grok 4.6 for more complex tasks – more on that in the model lineup below.&lt;/p&gt;
&lt;h2&gt;My model lineup&lt;/h2&gt;
&lt;p&gt;I have several models enabled and switch between them depending on the task:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;What I use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek v4 Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Default for most of my work – runs through my OpenCode Go subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek v4 Flash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lighter, repetitive tasks like scraping or bulk file operations – fast and cheap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grok 4.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;More complex tasks – runs through my SuperGrok plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GPT-5.6 Luna&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vision model for pi-vision-handoff – runs through the Codex free plan or OpenCode Go&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Switching is instant – &lt;code&gt;Ctrl+L&lt;/code&gt; opens the model selector, and &lt;code&gt;Ctrl+P&lt;/code&gt; cycles through scoped models.&lt;/p&gt;
&lt;h2&gt;The interface and my custom footer&lt;/h2&gt;
&lt;p&gt;When you open Pi, the interface is clean. The editor is at the bottom, the conversation in the middle, and the footer shows your working directory, git branch, current model, thinking level, and context window usage.&lt;/p&gt;
&lt;p&gt;One thing I customized early on was the footer. The default footer shows more information than I need. So I asked Pi itself to build me a custom minimal footer. I described what I wanted – current folder, git branch with a dirty marker, model name, thinking level, an optional goal slot, and a compact context bar – and it wrote the TypeScript extension for me.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Custom minimal footer extension for Pi agent&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/pi-agent-setup/custom-extension.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;The extension lives at &lt;code&gt;~/.pi/agent/extensions/minimal-footer.ts&lt;/code&gt; and Pi loads it automatically on every startup. I have also put &lt;a href=&quot;https://gist.github.com/deepakness/81e716c4654d22bee6b3a830553ec004&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the code in a gist&lt;/a&gt; – just drop it into the extensions folder and restart Pi. It was a small change but it made the experience feel personal. I also learned a bit about how Pi extensions work in the process.&lt;/p&gt;
&lt;p&gt;I recently added another small extension called &lt;a href=&quot;https://gist.github.com/deepakness/cebf2c46c4c3158f6a4484bd2a058f6d&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Whimsical working messages&lt;/a&gt;, based on &lt;a href=&quot;https://github.com/mitsuhiko/agent-stuff/blob/main/extensions/whimsical.ts&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Armin Ronacher&#39;s original whimsical extension&lt;/a&gt;. I customized it with more colors, decorative symbols, and rotating status phrases that change every two seconds while Pi is thinking. The extension lives at &lt;code&gt;~/.pi/agent/extensions/whimsical.ts&lt;/code&gt; and can be installed by copying my gist into the extensions folder.&lt;/p&gt;
&lt;h2&gt;Packages I installed&lt;/h2&gt;
&lt;p&gt;Pi packages are npm or git packages that bundle extensions, skills, prompts, and themes. You install them with &lt;code&gt;pi install&lt;/code&gt;. Here are the ones I use:&lt;/p&gt;
&lt;h3&gt;1. pi-web-access&lt;/h3&gt;
&lt;p&gt;This package gives Pi the ability to search the web, fetch page content, extract YouTube video transcripts, and explore GitHub repos. You can find it at &lt;a href=&quot;https://pi.dev/packages/pi-web-access&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pi.dev/packages/pi-web-access&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pi &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; npm:pi-web-access&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works out of the box with Exa search (no API key needed), and you can optionally add API keys for Perplexity, Exa, or Gemini for more control. I configured it to use Exa as the default search provider by creating a &lt;code&gt;~/.pi/web-search.json&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;provider&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;exa&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;allowBrowserCookies&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;summaryModel&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;opencode-go/deepseek-v4-flash&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;workflow&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;none&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;curatorTimeoutSeconds&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this package, Pi can search the web, read documentation, understand YouTube videos, and clone and explore GitHub repositories – all without leaving the terminal.&lt;/p&gt;
&lt;h3&gt;2. pi-vision-handoff&lt;/h3&gt;
&lt;p&gt;DeepSeek cannot look at images – it has no vision capabilities. I use the &lt;a href=&quot;https://pi.dev/packages/pi-vision-handoff&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pi-vision-handoff&lt;/a&gt; package, which describes images with a vision model and hands the text description back to the non-vision model.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pi &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; npm:pi-vision-handoff&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have set up GPT-5.6 Luna as my vision model through the Codex free plan, which has surprisingly generous free limits. I can also run it through my OpenCode Go subscription. Whenever I paste an image into Pi, Luna describes it, and then DeepSeek uses that description to finish the task. It works well for the occasional screenshot or design reference I throw at it.&lt;/p&gt;
&lt;p&gt;The handoff approach is provider-agnostic, so I can pick any vision-capable model from the &lt;code&gt;/vision-handoff&lt;/code&gt; picker and every text-only model I use can suddenly see images. Descriptions are cached per image and batched into a single vision call, so repeated screenshots don&#39;t get re-analyzed.&lt;/p&gt;
&lt;p&gt;I have also tried pi-codex-goal for goal tracking, pi-agent-browser-native for browser tasks inside the terminal, and pi-cursor-sdk for using my Cursor subscription inside Pi, but I don&#39;t use them anymore. I wrote about the Cursor setup in &lt;a href=&quot;https://deepakness.com/raw/pi-composer-2-5/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my raw notes&lt;/a&gt; and the &lt;a href=&quot;https://deepakness.com/raw/pi-cursor-codex-scraping/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;long-running /goal tasks&lt;/a&gt; as well.&lt;/p&gt;
&lt;h2&gt;Project instructions with AGENTS.md and APPEND_SYSTEM.md&lt;/h2&gt;
&lt;p&gt;Pi has two ways to give the model instructions: &lt;code&gt;AGENTS.md&lt;/code&gt; for project-specific context, and &lt;code&gt;APPEND_SYSTEM.md&lt;/code&gt; for global behavioral rules.&lt;/p&gt;
&lt;h3&gt;AGENTS.md (project context)&lt;/h3&gt;
&lt;p&gt;Pi loads &lt;code&gt;AGENTS.md&lt;/code&gt; files at startup and injects them into the system prompt. There are three discovery locations – a global one at &lt;code&gt;~/.pi/agent/AGENTS.md&lt;/code&gt;, then parent directories walking up, and finally the current directory – and they&#39;re all concatenated together.&lt;/p&gt;
&lt;p&gt;I keep my global AGENTS.md minimal. It only describes what stack I typically work with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Global Pi Instructions

- Projects commonly use Laravel (PHP/Inertia/React), Next.js
  (TypeScript/Tailwind), or Astro. Check the project-root AGENTS.md for
  stack-specific rules – if none exists, ask.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &amp;quot;if none exists, ask&amp;quot; part is important. When I&#39;m working in a plain HTML/CSS/JS folder or some other stack, the agent doesn&#39;t force a framework on me – it just asks.&lt;/p&gt;
&lt;p&gt;Most of my projects also have their own &lt;code&gt;AGENTS.md&lt;/code&gt; with framework-specific rules, but I rarely write them by hand. They&#39;re generated by the framework tooling (like Laravel Boost) with the exact package versions and conventions for that project.&lt;/p&gt;
&lt;h3&gt;APPEND_SYSTEM.md (global behavioral rules)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is for project context, but some rules apply across every project regardless of stack. That&#39;s where &lt;code&gt;APPEND_SYSTEM.md&lt;/code&gt; comes in. It lives at &lt;code&gt;~/.pi/agent/APPEND_SYSTEM.md&lt;/code&gt; and gets appended directly to the system prompt – which means these rules carry higher authority than AGENTS.md instructions.&lt;/p&gt;
&lt;p&gt;Here&#39;s what I put in mine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- If the default provider doesn&#39;t have vision capabilities, use
  pi-vision-handoff for images.
- Read relevant local files first when the answer is available in the
  codebase. If not, research online via pi-web-access. Before making a
  big change based on online research findings, confirm with me first.
- Explain risky file edits and destructive commands before executing.
- Write simply. Avoid AI-slop language – no flowery adjectives,
  unnecessary adverbs, or overly formal phrasing.
- Use en dashes (–) not em dashes (—).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few notes on these rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The vision handoff rule is now provider-agnostic. I might use DeepSeek, Grok, or another model as my default, so instead of hardcoding a provider name, it checks whether the current model has vision capabilities.&lt;/li&gt;
&lt;li&gt;The second rule is more explicit than my earlier version. Instead of just saying &amp;quot;don&#39;t search for things you already know&amp;quot;, it tells the agent to read local files first, search online only when needed, and confirm before making big changes based on online research.&lt;/li&gt;
&lt;li&gt;The en dash rule is a simple mechanical preference that keeps my writing style consistent across all sessions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Keyboard shortcuts and commands I use&lt;/h2&gt;
&lt;p&gt;Pi has many shortcuts. Here are the ones I use regularly:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open the model selector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+P&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cycle through scoped models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Shift+Tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Change thinking level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ctrl+C&lt;/code&gt; (twice)&lt;/td&gt;
&lt;td&gt;Quit Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Escape&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cancel or abort the current action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send as a steering message (interrupts current work)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Alt+Enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Send as a follow-up message (delivered after work finishes)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The difference between Enter and Alt+Enter took some time to get used to. If I want Pi to change direction mid-work, I press Enter. If I just want to add a note for after it finishes, I use Alt+Enter.&lt;/p&gt;
&lt;p&gt;And these are the commands I use, often by typing &lt;code&gt;/&lt;/code&gt; in the editor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/model&lt;/code&gt; – Switch models without remembering the shortcut&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/settings&lt;/code&gt; – Change thinking level, theme, and other settings&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/resume&lt;/code&gt; – Pick up from a previous session&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/new&lt;/code&gt; – Start a fresh session&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/tree&lt;/code&gt; – See the full session history and jump to any point&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/compact&lt;/code&gt; – Summarize old messages to free up context&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/session&lt;/code&gt; – Show current session info like tokens and cost&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;/tree&lt;/code&gt; command is especially useful. You can see your entire conversation history like a tree, jump to any branch, and continue from there. It&#39;s great when you want to try a different approach without losing what you had before. I got very curious about it after coming across &lt;a href=&quot;https://x.com/dillon_mulroy/status/2060018957227061299&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this shared Pi session&lt;/a&gt; from Dillon Mulroy. I later wrote down everything I learned from that session in &lt;a href=&quot;https://deepakness.com/raw/pi-agent-lessons-from-dillon/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my raw notes&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;A few things I learned while using Pi&lt;/h2&gt;
&lt;p&gt;Long sessions fill up the context window. Pi has automatic compaction that summarizes older messages when you&#39;re running out of space. You can also do it manually with &lt;code&gt;/compact&lt;/code&gt;. And I love the &lt;code&gt;pi --continue&lt;/code&gt; command, that directly opens the last chat in the repo/folder.&lt;/p&gt;
&lt;p&gt;Another thing I like is the steering system. While Pi is working (running bash commands, editing files, etc.), you can type a new message and press Enter. This queues a steering message that gets delivered after the current tool finishes. I use this when I notice Pi going in the wrong direction mid-work and want to correct it quickly. Sometimes, instead of a steering message, I also do &lt;code&gt;Option+Enter&lt;/code&gt; that sends the message after previous output is completely finished generating.&lt;/p&gt;
&lt;p&gt;You can also use many providers inside Pi, including OpenAI, Cursor, OpenRouter, xAI, Claude, and more through their APIs.&lt;/p&gt;
&lt;h2&gt;Things I like about Pi&lt;/h2&gt;
&lt;p&gt;After using Pi for a few weeks, here&#39;s what stands out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Simple and fast:&lt;/strong&gt; The terminal interface is snappy. No Electron, no UI lag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customizable:&lt;/strong&gt; I built the exact footer I wanted, picked the packages I need, and the rest stays minimal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Works with any provider:&lt;/strong&gt; I use DeepSeek for most of my work through OpenCode Go, and switching between models and providers is one shortcut away. I also learn a lot when using it – you get closer to how things actually work, unlike more polished tools where everything is hidden.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session tree is brilliant:&lt;/strong&gt; Being able to branch conversations and go back to any point is something I miss when using other tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open source (MIT):&lt;/strong&gt; I like that no company decides what features I get or how much I pay.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compaction just works:&lt;/strong&gt; I don&#39;t think about context limits. Pi handles it in the background, faster and better than others do.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I currently use Pi with DeepSeek for most of my work, Grok Build for some complex tasks, and Claude Code mainly for frontend and UI work. I have used Cursor and Codex for main project work, and both remain polished tools that do their jobs well.&lt;/p&gt;
&lt;p&gt;But Pi has earned a permanent place as my daily driver. It&#39;s fast, it&#39;s cheap (especially with DeepSeek through my OpenCode Go subscription), and the combination of Pi + a few well-chosen packages has been solid so far.&lt;/p&gt;
&lt;p&gt;Here&#39;s what my current setup looks like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DeepSeek v4 Pro with &lt;code&gt;max&lt;/code&gt; thinking (default, via OpenCode Go)&lt;/li&gt;
&lt;li&gt;DeepSeek v4 Flash for lighter tasks&lt;/li&gt;
&lt;li&gt;Grok 4.6 for more complex tasks via SuperGrok&lt;/li&gt;
&lt;li&gt;pi-web-access for web search and content extraction via Exa&lt;/li&gt;
&lt;li&gt;pi-vision-handoff to handle images via GPT-5.6 Luna (Codex free plan or OpenCode Go)&lt;/li&gt;
&lt;li&gt;APPEND_SYSTEM.md with writing style and behavior rules&lt;/li&gt;
&lt;li&gt;A custom minimal footer extension&lt;/li&gt;
&lt;li&gt;Whimsical working messages extension with rotating status phrases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&#39;s it. I tried more packages but kept only what I actually use.&lt;/p&gt;
&lt;p&gt;If you are comfortable in the terminal and like the idea of a minimal, extensible agent, &lt;a href=&quot;https://pi.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;give Pi a try&lt;/a&gt;. Start with the basics and add packages as you feel the need.&lt;/p&gt;
&lt;p&gt;I have been using Pi for a few months now and the setup is stable. I uninstalled a few extensions that didn&#39;t add enough value. The simpler the setup, the better.&lt;/p&gt;
&lt;p&gt;I will keep updating this post as I discover new things about Pi.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Self-hosting FreshRSS on a Raspberry Pi device</title>
    <link href="https://deepakness.com/blog/freshrss-on-pi/" />
    <updated>2026-03-09T00:18:03Z</updated>
    <id>https://deepakness.com/blog/freshrss-on-pi/</id>
    <summary>A quick guide to self-hosting FreshRSS, an open-source feed reader, on the Raspberry Pi 4B with 1GB of RAM.</summary>
    <content type="html">&lt;p&gt;I love &lt;a href=&quot;https://courses.ischool.berkeley.edu/i290-14/s05/lecture-23/allslides.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;RSS/Atom&lt;/a&gt; and am subscribed to at least 75 feeds via the &lt;a href=&quot;https://netnewswire.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;NetNewsWire app&lt;/a&gt; on my macOS laptop. While the app works great, sometimes, I want to read blogs on my phone too and my current setup didn&#39;t allow that. I had been thinking of solving this problem for a long time, and finally got the idea and time to build a better reading setup when I learned about &lt;a href=&quot;https://github.com/FreshRSS/FreshRSS&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;FreshRSS&lt;/a&gt; and that it can also be self-hosted.&lt;/p&gt;
&lt;p&gt;Initially, I was ready to self-host this on a tiny VPS, but then I decided to use the tiny &lt;a href=&quot;https://www.raspberrypi.com/products/raspberry-pi-4-model-b/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Raspberry Pi 4B with 1GB RAM&lt;/a&gt; that I had. Now that the setup is ready and I am already using it, it only consumes &amp;lt;250MB of RAM on the Pi device.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Raspberry Pi 4B with 1GB RAM&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/freshrss-on-pi/r-pi-4b-1gb.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;In this post, I will note down the whole installation process and my experience at the end:&lt;/p&gt;
&lt;h2&gt;1. Installing Docker&lt;/h2&gt;
&lt;p&gt;Docker made the process of installing and maintaining FreshRSS easier, and it was also the &lt;a href=&quot;https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;recommended way&lt;/a&gt; to install the app.&lt;/p&gt;
&lt;p&gt;I, first, connected to my Pi device from my macOS laptop via SSH and then ran the following commands to install and start Docker.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Refresh package lists&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update

&lt;span class=&quot;token comment&quot;&gt;# Install Docker engine + CLI + Compose&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; docker.io docker-cli &lt;span class=&quot;token function&quot;&gt;docker-compose&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Start Docker now and on boot&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--now&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Test iif everything is working&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose version
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run hello-world&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Earlier, my device was using ~100MB of RAM when it had nothing installed, and now it was using ~180-ish MB of RAM.&lt;/p&gt;
&lt;h2&gt;2. Installing FreshRSS on the Pi&lt;/h2&gt;
&lt;p&gt;As Docker was ready, I proceeded with installing the FreshRSS app.&lt;/p&gt;
&lt;p&gt;First, I created a new folder in the root called &lt;code&gt;freshrss&lt;/code&gt; and a few subfolders by running the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Create a new folder and subfolders&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; ~/freshrss/data ~/freshrss/extensions

&lt;span class=&quot;token comment&quot;&gt;# Go into the newly created folder&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; ~/freshrss&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Created the &lt;code&gt;compose.yaml&lt;/code&gt; file in the &lt;code&gt;freshrss&lt;/code&gt; directory with the following content. I used the &lt;code&gt;nano compose.yaml&lt;/code&gt; command to create the file, and then pasted the following:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;freshrss&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; freshrss/freshrss&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; freshrss
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;127.0.0.1:8080:80&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;TZ&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Asia/Kolkata
      &lt;span class=&quot;token key atrule&quot;&gt;CRON_MIN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;13,43&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/var/www/FreshRSS/data
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./extensions&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/var/www/FreshRSS/extensions
    &lt;span class=&quot;token key atrule&quot;&gt;healthcheck&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CMD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;cli/health.php&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 75s
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10s
      &lt;span class=&quot;token key atrule&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;start_period&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 60s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might need to modify the TZ (timezone) value, as per your location. And then started the container:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Start the docker container&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Check status using these commands&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose &lt;span class=&quot;token function&quot;&gt;ps&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything works, exit using &lt;code&gt;ctrl + c&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now, the app is ready and running at the port &lt;code&gt;8080&lt;/code&gt;, but you can&#39;t access it from your different devices yet. From here, you have 2 options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use Tailscale on all your devices to access&lt;/li&gt;
&lt;li&gt;Expose the app to the &lt;a href=&quot;https://deepakness.com/raw/local-apps-to-internet/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;internet using Cloudflare Tunnel&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I will explain the Cloudflare Tunnel method here.&lt;/p&gt;
&lt;h2&gt;3. Exposing the app to the public internet&lt;/h2&gt;
&lt;p&gt;First, I needed to install &lt;code&gt;cloudflared&lt;/code&gt; on my Pi, so I ran the following commands one by one:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--mode&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;0755 /usr/share/keyrings
&lt;span class=&quot;token function&quot;&gt;curl&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fsSL&lt;/span&gt; https://pkg.cloudflare.com/cloudflare-main.gpg &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; /usr/share/keyrings/cloudflare-main.gpg &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;/dev/null

&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/cloudflared.list

&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; cloudflared&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also had to sign up to my Cloudflare account, go to &lt;code&gt;Networking &amp;gt; Tunnels&lt;/code&gt; 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:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Connect to the Cloudflare dashboard&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; cloudflared &lt;span class=&quot;token function&quot;&gt;service&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; a_very_long_string_here

&lt;span class=&quot;token comment&quot;&gt;# Keep the tunnel running&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; systemctl &lt;span class=&quot;token builtin class-name&quot;&gt;enable&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--now&lt;/span&gt; cloudflared&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this I added a destination subdomain, and then added &lt;code&gt;http://localhost:8080&lt;/code&gt; as the service URL, and the app was live.&lt;/p&gt;
&lt;p&gt;I opened the destination subdomain I set up in the browser. A FreshRSS installation checklist was shown, and then it asked me to sign up for the admin account. I was in. I tried adding an RSS feed for testing, and it was working.&lt;/p&gt;
&lt;h2&gt;4. Importing my reading list and final look&lt;/h2&gt;
&lt;p&gt;I maintain this &lt;a href=&quot;https://deepakness.com/blogroll/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;blogroll page&lt;/a&gt; on my website, so I added these feeds to the app. And then I also imported a &lt;code&gt;.opml&lt;/code&gt; file &lt;a href=&quot;https://x.com/karpathy/status/2018043254986703167&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;shared by Andrej Karpathy&lt;/a&gt; earlier, it contained &lt;a href=&quot;https://gist.github.com/emschwartz/e6d2bf860ccc367fe37ff953ba6de66b&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;90+ RSS/Atom feeds&lt;/a&gt; of most popular blogs on HackerNews.&lt;/p&gt;
&lt;p&gt;I probably don&#39;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;FreshRSS final look for me&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/freshrss-on-pi/freshrss-final-look.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;How the Raspberry Pi 4B is performing&lt;/h2&gt;
&lt;p&gt;The Pi device is performing surprisingly well. Currently, my friend and I are both using the self-hosted RSS reader, and it doesn&#39;t even lag or slow down, even for a bit. It still uses less than 250 MB of RAM, sometimes even less than 200 MB, as you see below:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;RAM usage for Pi 4B&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/freshrss-on-pi/ram-usage-for-pi-4b.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Raspberry Pi connected to my WiFi router&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/freshrss-on-pi/pi-connected-to-router.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;What I like about FreshRSS&lt;/h2&gt;
&lt;p&gt;Some things I love about the &lt;a href=&quot;https://freshrss.org/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;open-source FreshRSS&lt;/a&gt; app are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It fetches feed updates on the server-side in the background, so I don&#39;t have to wait for 2 minutes for all 90 feeds to get refreshed. When I open the public URL in the browser, it&#39;s ready for me to read as new updates are already fetched.&lt;/li&gt;
&lt;li&gt;When you mark a post as favorite, it&#39;s permanently saved in the SQLite database it uses. Even if the original version is deleted, you still have the locally saved version.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;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.&lt;/p&gt;
</content>
  </entry><entry>
    <title>How I Sync Mastodon Posts with My 11ty Blog</title>
    <link href="https://deepakness.com/blog/mastodon-sync/" />
    <updated>2026-02-06T16:52:48Z</updated>
    <id>https://deepakness.com/blog/mastodon-sync/</id>
    <summary>A walkthrough of a Node.js script that maps Mastodon statuses to blog slugs and updates frontmatter.</summary>
    <content type="html">&lt;p&gt;I don&#39;t have a comment system on my blog, so I started adding this &lt;a href=&quot;https://deepakness.com/raw/comment-via-email/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;comment via email&lt;/a&gt; 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 &amp;quot;Comment on Mastodon&amp;quot; feature below my blog posts (along with &lt;a href=&quot;https://deepakness.com/blog/adding-webmentions/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;webmentions&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Comment on Mastodon link in the footer&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/mastodon-sync/comment-on-mastodon-link.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Typically, here&#39;s the process that I would follow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Publish a blog post&lt;/li&gt;
&lt;li&gt;Share it on Mastodon, and&lt;/li&gt;
&lt;li&gt;Add Mastodon post link in Markdown frontmatter&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you add the &lt;code&gt;mastodon&lt;/code&gt; YAML frontmatter in a post, the &amp;quot;Comment on Mastodon&amp;quot; option automatically starts showing below the post. Here&#39;s what the frontmatter looks like:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;2025-01-01&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;title&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;description&quot;&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;tags&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; tag&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;one
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But... it&#39;s a tedious process to manually do it each time, isn&#39;t it?&lt;/p&gt;
&lt;p&gt;I wanted something semi-automated, and I have finally put together a workflow that works for me.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I publish a blog post.&lt;/li&gt;
&lt;li&gt;It&#39;s automatically published to Mastodon, Threads, and Bluesky via &lt;a href=&quot;https://go.deepakness.com/n8n&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;n8n&lt;/a&gt; by using &lt;a href=&quot;https://go.deepakness.com/typefully&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Typefully&lt;/a&gt; API (&lt;a href=&quot;https://deepakness.com/raw/typefully-api-n8n/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;see how it&#39;s done&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Run a script to auto-fetch and find your Mastodon posts with your blog URLs, and update the frontmatter.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;I already use &lt;a href=&quot;https://go.deepakness.com/typefully&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Typefully&lt;/a&gt; to schedule my posts on social media, so it&#39;s not an additional purchase for me. But you can automate publishing to Mastodon directly via their API.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I am going to paste the entire script that I use for this, you can save it inside, say, &lt;code&gt;/scripts/mastodon.js&lt;/code&gt; file.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fileURLToPath &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;url&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; __filename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fileURLToPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; __dirname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__filename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;ROOT_DIR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__dirname&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;..&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONTENT_DIR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ROOT_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;content&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;MASTODON_INSTANCE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://mastodon.social&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;MASTODON_USERNAME&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;deepakness&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BLOG_DOMAIN&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;deepakness.com&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;DRY_RUN&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;argv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;--dry-run&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;FORCE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;argv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;--force&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Fetch account ID from username&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchAccountId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;MASTODON_INSTANCE&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/api/v1/accounts/lookup?acct=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;MASTODON_USERNAME&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Failed to fetch account: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; account &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; account&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Fetch all statuses with pagination&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchAllStatuses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;accountId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; statuses &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; maxId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; page &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;MASTODON_INSTANCE&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/api/v1/accounts/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;accountId&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/statuses&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;searchParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;limit&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;40&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;searchParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;exclude_reblogs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;true&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;maxId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;searchParams&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;max_id&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; maxId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Fetching page &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;page&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Failed to fetch statuses: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; batch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;batch&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    statuses&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;batch&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    maxId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; batch&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;batch&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    page&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Small delay to be nice to the API&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;resolve&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setTimeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; statuses&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Extract links from HTML content using regex&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractLinksFromHtml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; links &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; regex &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;href=&quot;([^&quot;]+)&quot;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;match &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; regex&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;html&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    links&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;match&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; links&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Parse deepakness.com URL and extract type (blog/raw) and slug&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseDeepakNessUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parsed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BLOG_DOMAIN&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname &lt;span class=&quot;token operator&quot;&gt;!==&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;www.&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BLOG_DOMAIN&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Match /blog/slug or /raw/slug&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; match &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^&#92;/(blog|raw)&#92;/([^/]+)&#92;/?$&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;match&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Find markdown file for a given type and slug&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findMarkdownFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; slug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; possiblePaths &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONTENT_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;index.md&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONTENT_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.md&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; filePath &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; possiblePaths&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; filePath&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Check if frontmatter already has mastodon field&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hasMastodonField&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; frontmatterMatch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^---&#92;n([&#92;s&#92;S]*?)&#92;n---&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;frontmatterMatch&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^mastodon:&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;m&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;frontmatterMatch&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Insert mastodon field into frontmatter&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;insertMastodonField&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; mastodonUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Insert after the opening ---&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^---&#92;n&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;---&#92;nmastodon: &quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;mastodonUrl&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&#92;n&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scan content directories and find files (optionally only those missing mastodon field)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findContentFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;onlyMissing &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; contentTypes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;blog&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;raw&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; type &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; contentTypes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; typeDir &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONTENT_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;typeDir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; entries &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;typeDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;withFileTypes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; entry &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; entries&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; filePath&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isDirectory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        filePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;typeDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;index.md&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.md&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        filePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;typeDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf-8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hasMastodon &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;hasMastodonField&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;onlyMissing &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;hasMastodon&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; slug &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isDirectory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.md&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        files&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; hasMastodon &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; files&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Main function&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n🐘 Syncing Mastodon links to markdown files...&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;DRY_RUN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;🔍 DRY RUN MODE - No files will be modified&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FORCE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;🔄 FORCE MODE - Will overwrite existing mastodon fields&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// First, check which files need updating&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Scanning content files...&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; filesToUpdate &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findContentFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FORCE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; label &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;FORCE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;files to check&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;files missing mastodon field&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Found &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;filesToUpdate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;label&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filesToUpdate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;✨ All files already have mastodon field. Nothing to do!&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Create a set for quick lookup&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; updateSet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filesToUpdate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;f&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;f&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Fetch account ID&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Looking up account @&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;MASTODON_USERNAME&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; accountId &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchAccountId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Found account ID: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;accountId&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Fetch all statuses&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Fetching all statuses...&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; statuses &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchAllStatuses&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;accountId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Found &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;statuses&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; statuses&#92;n&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Build map of blog URLs to mastodon URLs (earliest status per slug)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; matchMap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// key: &quot;type/slug&quot;, value: match object&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; status &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; statuses&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; links &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;extractLinksFromHtml&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; link &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; links&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; parsed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseDeepakNessUrl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;link&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;parsed&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; key &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// Always overwrite to keep the earliest (last in reverse-chronological order)&lt;/span&gt;
        matchMap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;slug&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; parsed&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;mastodonUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; status&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;blogUrl&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; link&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Filter to only matches for files that need updating&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; matches &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Array&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;matchMap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;values&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; updateSet&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;has&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Found &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;matches&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; Mastodon posts for files needing update&#92;n&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Process matches&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; updated &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; skipped &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; match &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; matches&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; filePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findMarkdownFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; relativePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;relative&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ROOT_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; filePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf-8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Check if file already has this exact mastodon URL (skip if same)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; existingMatch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^mastodon:&#92;s*&quot;([^&quot;]+)&quot;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;m&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;existingMatch &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; existingMatch&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mastodonUrl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;⏭️  Already correct: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;relativePath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      skipped&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Remove existing mastodon field if present (for --force)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^mastodon:&#92;s*&quot;[^&quot;]+&quot;&#92;n&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;m&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    newContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;insertMastodonField&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newContent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mastodonUrl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;DRY_RUN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;📝 Would update: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;relativePath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;   Mastodon URL: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mastodonUrl&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newContent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;✅ Updated: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;relativePath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    updated&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Files that had no matching Mastodon post&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; notFound &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; filesToUpdate&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; updated &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; skipped&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n✨ Done!&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;   Updated: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;updated&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;skipped &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;   Already correct: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;skipped&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;   No Mastodon post found: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;notFound&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;❌ Error:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above Node.js script uses public Mastodon API endpoints that do not require authentication, so you don&#39;t even need an API key. It uses &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, and &lt;code&gt;fetch&lt;/code&gt; libraries which are Node.js built-ins.&lt;/p&gt;
&lt;p&gt;When I run the script in my terminal by running either &lt;code&gt;npm run mastodon&lt;/code&gt; or &lt;code&gt;node scripts/mastodon.js&lt;/code&gt; command in my terminal:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Scans my local markdown files in &lt;code&gt;content/blog/&lt;/code&gt; and &lt;code&gt;content/raw/&lt;/code&gt; folders, identifies which ones are missing a &lt;code&gt;mastodon&lt;/code&gt;: field in their YAML frontmatter.&lt;/li&gt;
&lt;li&gt;Fetches all posts from the &lt;code&gt;@deepakness&lt;/code&gt; Mastodon account via the public API by paginating through all posts I have.&lt;/li&gt;
&lt;li&gt;Matches posts to blog entries by scanning each Mastodon post&#39;s HTML content for links to &lt;code&gt;deepakness.com/blog/[slug]&lt;/code&gt; or &lt;code&gt;deepakness.com/raw/[slug]&lt;/code&gt;, and then maps those back to the related local markdown files.&lt;/li&gt;
&lt;li&gt;Updates the frontmatter of each matched markdown file by inserting a &lt;code&gt;mastodon: &amp;quot;https://mastodon.social/...&amp;quot;&lt;/code&gt; field.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The script also has two optional flags:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--dry-run&lt;/code&gt; shows what would change without writing anything&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--force&lt;/code&gt; overwrites existing &lt;code&gt;mastodon:&lt;/code&gt; fields instead of skipping files that already have one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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 wins).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Most probably, you won&#39;t be able to directly use this script as it&#39;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.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I manually run the script every few days, so you won&#39;t see the &amp;quot;Comment on Mastodon&amp;quot; link on new posts right away – but it shows up soon enough.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Dynamic Open Graph Images for My 11ty Blog</title>
    <link href="https://deepakness.com/blog/custom-og-images/" />
    <updated>2026-02-06T09:18:45Z</updated>
    <id>https://deepakness.com/blog/custom-og-images/</id>
    <summary>A quick guide to how I dynamically generate custom Open Graph images for each post on my 11ty blog, using satori.</summary>
    <content type="html">&lt;p&gt;Apart from &lt;a href=&quot;https://deepakness.com/blog/adding-webmentions/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;adding webmentions&lt;/a&gt; and &lt;a href=&quot;https://deepakness.com/blog/better-images/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;better handling images&lt;/a&gt;, 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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;In this post, I will show the exact setup I am using.&lt;/p&gt;
&lt;h2&gt;1. How OG image is selected on my website&lt;/h2&gt;
&lt;p&gt;I use computed data to decide what image goes into &lt;code&gt;og:image&lt;/code&gt; and &lt;code&gt;twitter:image&lt;/code&gt; and have this file &lt;code&gt;_data/eleventyComputed.js&lt;/code&gt; with the following content:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;ogImage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// If the post has a custom image in frontmatter, use that&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// For blog posts and raw notes, use the co-located og.jpeg&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;page &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/blog/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/raw/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;og.jpeg&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Fall back to default metadata image&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And in my base template, &lt;code&gt;_includes/layouts/base.njk&lt;/code&gt;, both Open Graph and Twitter tags read this value:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;OG value from base.njk&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/custom-og-images/og-value-base-njk.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;So the priority is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt; from frontmatter (manual custom image)&lt;/li&gt;
&lt;li&gt;Generated co-located &lt;code&gt;og.jpeg&lt;/code&gt; for &lt;code&gt;/blog/*&lt;/code&gt; and &lt;code&gt;/raw/*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Site default fallback image from metadata&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt=&quot;Final OG image URL value&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/custom-og-images/final-url-value.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;And then the final Open Graph image URL looks like the one in the screenshot above.&lt;/p&gt;
&lt;h2&gt;2. The generator script I use&lt;/h2&gt;
&lt;p&gt;I use &lt;code&gt;sharp&lt;/code&gt; image library with a Node.js script at &lt;code&gt;scripts/generate-og-images.js&lt;/code&gt;. And the script does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Scans &lt;code&gt;content/blog&lt;/code&gt; and &lt;code&gt;content/raw&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Scans top-level pages in &lt;code&gt;content/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Handles homepage as a special case&lt;/li&gt;
&lt;li&gt;Skips items that already have &lt;code&gt;og.png&lt;/code&gt;, &lt;code&gt;og.jpeg&lt;/code&gt;, or &lt;code&gt;og.jpg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Skips items that already have &lt;code&gt;image&lt;/code&gt; in frontmatter&lt;/li&gt;
&lt;li&gt;Uses MD5 cache in &lt;code&gt;_data/cache/og-cache.json&lt;/code&gt; to skip unchanged items&lt;/li&gt;
&lt;li&gt;Outputs &lt;code&gt;1200x630&lt;/code&gt; JPEG images&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It uses &lt;a href=&quot;https://github.com/vercel/satori&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vercel&#39;s satori library&lt;/a&gt; for layout, &lt;code&gt;@resvg/resvg-js&lt;/code&gt; for rendering SVG to PNG, and &lt;code&gt;sharp&lt;/code&gt; to produce final JPEG. Here are some useful snippets from the &lt;code&gt;generate-og-images.js&lt;/code&gt; script.&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CACHE_FILE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rootDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;_data&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;cache&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;og-cache.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BLOG_DIR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rootDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;content&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;blog&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;RAW_DIR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rootDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;content&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;raw&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;PAGES_OG_DIR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rootDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;public&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;img&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;og&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hasExistingOg &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hasCustomImage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  skipped&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cache&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cacheKey&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;contentHash &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  cached&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-javascript&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; outputPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;collection &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pages&#39;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;PAGES_OG_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.jpeg&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;folderPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;og.jpeg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&#39;s a huge 600-line script, so I didn&#39;t paste it all here. But you can &lt;a href=&quot;https://gist.github.com/deepakness/eab8d275b51418f2c093151dff6d16b4&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;click this link to view&lt;/a&gt; the entire script.&lt;/p&gt;
&lt;h2&gt;3. Card design setup&lt;/h2&gt;
&lt;p&gt;I kept the design clean and consistent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;White background with a subtle grid pattern&lt;/li&gt;
&lt;li&gt;Blue accent strip at the top&lt;/li&gt;
&lt;li&gt;Large post title and optional description&lt;/li&gt;
&lt;li&gt;Footer with profile photo, name, and website label&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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&#39;s the OG image for this post, the one you&#39;re currently reading:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;OG image for this post&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/custom-og-images/og-for-this-post.jpeg&quot;&gt;&lt;/p&gt;
&lt;h2&gt;4. Commands and workflow&lt;/h2&gt;
&lt;p&gt;I just run the following command to generate images:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; run og&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In &lt;code&gt;package.json&lt;/code&gt;, my local start flow already includes OG generation:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&quot;start&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;npm run optimize-images &amp;amp;&amp;amp; npm run og &amp;amp;&amp;amp; npx @11ty/eleventy --serve --quiet&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So in day-to-day writing, I can quickly regenerate social cards before publishing, and also &lt;a href=&quot;https://deepakness.com/blog/better-images/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;optimize images on the go&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Terminal output after running npm run og script&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/custom-og-images/og-script-terminal-output.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;And above is the terminal output when I run the script.&lt;/p&gt;
&lt;h2&gt;5. Useful edge case: manual override&lt;/h2&gt;
&lt;p&gt;If I set an &lt;code&gt;image&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/blog/vivaldi-browser/og.jpg&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the full setup I am using right now.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Adding Webmentions to My 11ty Blog</title>
    <link href="https://deepakness.com/blog/adding-webmentions/" />
    <updated>2026-08-07T00:00:00Z</updated>
    <published>2026-02-03T12:10:53Z</published>
    <id>https://deepakness.com/blog/adding-webmentions/</id>
    <summary>A quick guide to sending and receiving webmentions to my 11ty blog.</summary>
    <content type="html">&lt;p&gt;I was thinking about this for a long time 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 &lt;a href=&quot;https://www.w3.org/TR/webmention/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;webmention protocol&lt;/a&gt; to completely understand it. But I did... and it&#39;s live on my website. You can &lt;a href=&quot;https://deepakness.com/raw/rss-feeds-are-cool-again/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;check out this post&lt;/a&gt; which has received some webmentions via Mastodon by the time of writing this post.&lt;/p&gt;
&lt;p&gt;When you talk about webmentions, receiving and sending them are two different things. And it becomes a little tricky to implement this on a static website like mine, which is built using &lt;a href=&quot;https://www.11ty.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;11ty static site generator&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In this post, I will explain my process of how I implemented both the receiving and sending of webmentions. Let&#39;s get to it.&lt;/p&gt;
&lt;h2&gt;1. Receive incoming webmentions&lt;/h2&gt;
&lt;p&gt;When other people link to your website from their blogs or interact with your blog posts on Mastodon or &lt;a href=&quot;https://en.wikipedia.org/wiki/Fediverse&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Fediverse&lt;/a&gt;, they send you webmentions that you need to receive to show on your website.&lt;/p&gt;
&lt;h3&gt;Using the webmention.io service&lt;/h3&gt;
&lt;p&gt;To receive, I used &lt;a href=&quot;https://webmention.io&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;webmention.io&lt;/a&gt;, created by &lt;a href=&quot;https://www.linkedin.com/in/aaronparecki/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Aaron Parecki&lt;/a&gt;, that automatically scans for incoming 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 the following &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags to your HTML. It looks like this on my blog:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;webmention&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://webmention.io/deepakness.com/webmention&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;pingback&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://webmention.io/deepakness.com/xmlrpc&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And after you add this, the webmention.io service starts collecting webmentions on your behalf. You can pull these and show them on your website via their API.&lt;/p&gt;
&lt;p&gt;From here, you need to get the webmention.io API key token that we will need in the next step.&lt;/p&gt;
&lt;h3&gt;Showing webmentions on my blog&lt;/h3&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;At first, I created a &lt;code&gt;webmentions.js&lt;/code&gt; file in my &lt;code&gt;_data&lt;/code&gt; folder with the following content:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dotenv/config&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; EleventyFetch &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;@11ty/eleventy-fetch&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; domain &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;deepakness.com&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; token &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;WEBMENTION_IO_TOKEN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;token&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;warn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;WEBMENTION_IO_TOKEN is missing – returning empty webmentions.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
    &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;https://webmention.io/api/mentions.jf2?domain=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;domain&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;amp;token=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;token&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;amp;per-page=1000&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Always fetch fresh on each build&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;EleventyFetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;6h&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I added the API key in the environment variable &lt;code&gt;WEBMENTION_IO_TOKEN&lt;/code&gt; on Netlify, as it will be used during the build time.&lt;/p&gt;
&lt;p&gt;Then I created a &lt;code&gt;comments.njk&lt;/code&gt; file (here are &lt;a href=&quot;https://gist.github.com/deepakness/782a1ff050ed52084eb5461bf8a7618a&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the entire contents of the file&lt;/a&gt;, including CSS) in the &lt;code&gt;_includes&lt;/code&gt; folder, and used it inside my post template like below:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Adding comments to post template&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/adding-webmentions/adding-comments-to-post-template.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;After this, I also used a service called &lt;a href=&quot;https://brid.gy&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Bridgy&lt;/a&gt; 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&#39;t natively support it.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Showing webmentions below posts&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/adding-webmentions/webmentions-below-posts.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Above is an example of my current design for showing webmentions below &lt;a href=&quot;https://deepakness.com/blog&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;posts&lt;/a&gt; and &lt;a href=&quot;https://deepakness.com/raw&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;notes&lt;/a&gt;. I also created a /webmention page that educates people on what this is and how this works on my blog.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The webmention sections on my blog only update when I publish something new and the site gets re-built on Netlify.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. Send outgoing webmention notifications&lt;/h2&gt;
&lt;p&gt;I am using this amazing tool &lt;a href=&quot;https://webmention.app&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;webmention.app&lt;/a&gt;, created by &lt;a href=&quot;https://remysharp.com/2019/06/18/send-outgoing-webmentions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Remy Sharp&lt;/a&gt;, that automates your outgoing webmentions.&lt;/p&gt;
&lt;p&gt;There are multiple ways to use the service, &lt;a href=&quot;https://webmention.app/docs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;as explained in the docs&lt;/a&gt;. I am integrating the service with Netlify by using the &amp;quot;deploy notification&amp;quot; feature.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open your project in Netlify&lt;/li&gt;
&lt;li&gt;Go to Project configuration&lt;/li&gt;
&lt;li&gt;Scroll down, click on Notifications&lt;/li&gt;
&lt;li&gt;Scroll to Deploy notifications under the Emails and webhooks section&lt;/li&gt;
&lt;li&gt;Click on Add notification, select HTTP POST request option&lt;/li&gt;
&lt;li&gt;In Event to listen for: select Deploy succeeded&lt;/li&gt;
&lt;li&gt;Enter below URL in URL to notify field&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-plaintext&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;https://webmention.app/check?token=[your-token]&amp;amp;limit=1&amp;amp;url=[your-feed-url]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img alt=&quot;Netlify deploy notification webhook&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/adding-webmentions/netlify-deploy-notification.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Make sure to replace &lt;code&gt;[your-token]&lt;/code&gt; with the token that you get from webmention.app, and &lt;code&gt;[your-feed-url]&lt;/code&gt; 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 &lt;a href=&quot;https://webmention.app/docs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;go through the docs&lt;/a&gt; carefully.&lt;/p&gt;
&lt;p&gt;Here are some useful resources that I found helpful during my setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mxb.dev/blog/using-webmentions-on-static-sites/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Using Webmentions in Eleventy&lt;/a&gt;, by Max Böck&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rowanmanning.com/posts/webmentions-for-your-static-site/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Webmentions for your Static Site&lt;/a&gt; by Rowan Manning&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://localghost.dev/blog/sending-webmentions-from-a-static-site/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Sending webmentions from a static site&lt;/a&gt; by Sophie&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paul.kinlan.me/using-web-mentions-in-a-static-sitehugo/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Using Web Mentions in a static site (Hugo)&lt;/a&gt; by Paul Kinlan&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Update (2026-08-07)&lt;/h2&gt;
&lt;p&gt;I later removed webmentions from this site entirely. The post above is still accurate as a historical write-up of how receiving (webmention.io + Bridgy) and sending (webmention.app) worked while the feature was live.&lt;/p&gt;
&lt;p&gt;For why I removed it, see &lt;a href=&quot;https://deepakness.com/raw/removing-webmentions/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this later note&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Starting a Newsletter using Listmonk</title>
    <link href="https://deepakness.com/blog/listmonk-setup/" />
    <updated>2026-02-04T00:00:00Z</updated>
    <published>2026-02-01T16:57:53Z</published>
    <id>https://deepakness.com/blog/listmonk-setup/</id>
    <summary>How I set up a newsletter with self-hosted Listmonk – first trying Amazon SES, then switching to SMTP2GO.</summary>
    <content type="html">&lt;p&gt;I have been thinking about starting and maintaining an email list for a long time, and today I finally made it live. It wasn&#39;t a straightforward process, but it wasn&#39;t extremely complicated either. I had to Google a lot and was finally able to set everything up as I wanted.&lt;/p&gt;
&lt;p&gt;For the setup, I used &lt;a href=&quot;https://listmonk.app&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Listmonk&lt;/a&gt; (self-hosted) for managing the list. I first tried &lt;a href=&quot;https://aws.amazon.com/ses/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Amazon SES&lt;/a&gt; for sending, then switched to &lt;a href=&quot;https://go.deepakness.com/smtp2go&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SMTP2GO&lt;/a&gt; after SES rejected my account (details near the end). That&#39;s still what I use today.&lt;/p&gt;
&lt;p&gt;But before I start, below is the form if you&#39;d like to subscribe to my list. I have kept it simple to match the aesthetics of my website. The form lives in the site footer on every page, and there&#39;s a fuller pitch on the &lt;a href=&quot;https://deepakness.com/list/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;newsletter page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, let&#39;s get to it.&lt;/p&gt;
&lt;h2&gt;What is Listmonk?&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;You control your data, your server, and how emails are sent, without paying per subscriber. It&#39;s great for newsletters, product updates, and simple email automation.&lt;/p&gt;
&lt;h2&gt;What is Amazon Simple Email Service (SES)?&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Self-hosting Listmonk&lt;/h2&gt;
&lt;p&gt;I hosted Listmonk on a Hetzner VPS (CX23, costs $3.5/mo) via &lt;a href=&quot;https://dokploy.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Dokploy&lt;/a&gt; and it took less than 5 minutes. Here are the steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create &lt;a href=&quot;https://go.deepakness.com/hetzner&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;an account on Hetzner&lt;/a&gt;, get verified, and create a new server. Even the cheapest server CX23 would be more than enough.&lt;/li&gt;
&lt;li&gt;SSH into the server, and install Dokploy by &lt;a href=&quot;https://docs.dokploy.com/docs/core/installation&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;following the instructions&lt;/a&gt; on their official website.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After the deployment is successful, visit the domain you added and register a new Listmonk account, and you should now be inside the app.&lt;/p&gt;
&lt;h2&gt;Getting the Amazon SES details&lt;/h2&gt;
&lt;p&gt;I didn&#39;t even have an AWS account so I was thinking it&#39;s going to be a complicated process, but it wasn&#39;t. I asked ChatGPT to guide me through the process, and it did. I just kept providing it with screenshots.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sign up or log in to the AWS account from here: &lt;a href=&quot;https://aws.amazon.com/ses/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://aws.amazon.com/ses/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SES asks you to add and verify your email address and sending domain as well. For example, add and verify &lt;code&gt;you@yoursite.com&lt;/code&gt; and also your domain &lt;code&gt;yoursite.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Verify your sending domain by adding a bunch of CNAME, DMARC, and SPF records at your domain registrar (or at Cloudflare if you&#39;re using it).&lt;/li&gt;
&lt;li&gt;Now, create SMTP credentials and get the SMTP username, SMTP password, and AWS region (you&#39;ll need these inside Listmonk).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I will suggest using ChatGPT, Gemini, or any other AI provider to guide you through everything.&lt;/p&gt;
&lt;h2&gt;Setting up Listmonk&lt;/h2&gt;
&lt;p&gt;Now, let&#39;s get through the process of setting up Listmonk:&lt;/p&gt;
&lt;h3&gt;1. General settings&lt;/h3&gt;
&lt;p&gt;First, you need to do some basic settings in the &lt;strong&gt;Settings &amp;gt; General&lt;/strong&gt; tab, as you see in the screenshot here.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Listmonk general settings&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/listmonk-general-settings.png&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Site name&lt;/strong&gt;: Add the name of your website where you will be showing the subscription form.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root URL&lt;/strong&gt;: 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
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://list.yoursite.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logo URL&lt;/strong&gt;: 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Favicon URL&lt;/strong&gt;: This is shown as the favicon on the public archive pages, if you choose to make your issues public.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Default &lt;code&gt;from&lt;/code&gt; Email&lt;/strong&gt;: Add your email that you want to send from.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Admin notification emails&lt;/strong&gt;: Add your email where you want to receive the notification when a campaign gets completed, and other admin related things.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. SMTP settings&lt;/h2&gt;
&lt;p&gt;You now have to do the SMTP settings and you will need your Amazon SES credentials that you created earlier. See the below screenshot for more info.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Listmonk SMTP settings&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/listmonk-smtp-settings.png&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Enabled&lt;/strong&gt;: Should be turned on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Host&lt;/strong&gt;: You need to add this, but replace the AWS_REGION here:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;email-smtp.AWS_REGION.amazonaws.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port&lt;/strong&gt;: Use 587 as the SMTP server port.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auth protocol&lt;/strong&gt;: Use LOGIN.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Username&lt;/strong&gt;: Paste the AWS SES username here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Password&lt;/strong&gt;: Paste the AWS SES password here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HELO hostname&lt;/strong&gt;: Leave it empty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLS&lt;/strong&gt;: Select STARTTLS here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;: Name anything you want.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And keep other options as the default. You can see the screenshot above for more info.&lt;/p&gt;
&lt;p&gt;You can test the setup by clicking on the &lt;strong&gt;Test Connection&lt;/strong&gt; 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).&lt;/p&gt;
&lt;h3&gt;3. Setting up the subscription form&lt;/h3&gt;
&lt;p&gt;Inside Listmonk, go to the &lt;strong&gt;Lists &amp;gt; All lists&lt;/strong&gt; 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&#39;s good for deliverability and ensures that only serious people join your list.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Creating a list in Listmonk&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/create-a-list.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once the list is ready, navigate to &lt;strong&gt;Lists &amp;gt; Forms&lt;/strong&gt; and you can either share the subscription form as the URL shown or get the HTML and embed it on your website.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Get the subscription form&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/subscription-form.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I have embedded the HTML on my website and it looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;newsletter-form&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;One email every week on AI, tech, and marketing finds I&#39;d tell a friend about.&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;form&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;action&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://listmonk.yoursite.com/subscription/form&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;listmonk-form&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;nonce&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;hidden&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;l&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;9dcc7bc2&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;placeholder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Your email&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;required&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;placeholder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Your name&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Subscribe&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;form&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;newsletter-note&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;No spam, just chaos.&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I have then added some CSS to make it look like how it currently looks in my website&#39;s footer. On mobile, form fields appear one above another.&lt;/p&gt;
&lt;h3&gt;4. Setting up a campaign template&lt;/h3&gt;
&lt;p&gt;I like sending plaintext emails so I had to create a new campaign template from &lt;strong&gt;Campaigns &amp;gt; Templates&lt;/strong&gt;, but if you&#39;re fine with the default campaign template (you can see the preview), there&#39;s no need for this step.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Actually, it&#39;s not absolutely plaintext because I want to use the HTML&#39;s hyperlink feature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt=&quot;Setting up the default Listmonk template&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/default-template.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;As you can see, it&#39;s just basic HTML for adding a hyperlink in &lt;strong&gt;Unsubscribe&lt;/strong&gt; and &lt;strong&gt;View in browser&lt;/strong&gt; links. I could use plaintext as well here, but the links look very ugly in the email.&lt;/p&gt;
&lt;h3&gt;5. Sending a test campaign&lt;/h3&gt;
&lt;p&gt;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 &lt;strong&gt;Campaigns &amp;gt; All campaigns&lt;/strong&gt;, delete the existing campaigns, and create a new one.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Creating a new campaign in Listmonk&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/create-new-campaign.png&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;: Name it anything, it&#39;s internal and not shown to the subscribers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subject&lt;/strong&gt;: The email subject that you want subscribers to receive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;From&lt;/strong&gt;: From address that you added in Settings earlier and that you want to send the newsletter emails from.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lists&lt;/strong&gt;: Select the list or lists of subscribers whom you want to send the emails to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Messenger&lt;/strong&gt;: Keep as &lt;code&gt;email&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Format&lt;/strong&gt;: Select Rich text as the format.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tags&lt;/strong&gt;: Optional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Send later&lt;/strong&gt;: If you want to schedule, use the Send later option and select date and time to send. Otherwise, keep it turned off.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt=&quot;Adding content for the new campaign&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/content-for-new-campaign.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, Save and go to the &lt;strong&gt;Content&lt;/strong&gt; tab, select Rich text as the format, select either the default template or the new template you created previously, and start writing your newsletter.&lt;/p&gt;
&lt;p&gt;As you see in the above screenshot, &lt;code&gt;.Subscriber.Name&lt;/code&gt; between double curly braces will be replaced by the actual name of the subscriber. You can learn more about template expressions &lt;a href=&quot;https://listmonk.app/docs/templating/#template-expressions&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;from this page&lt;/a&gt; in their docs.&lt;/p&gt;
&lt;p&gt;After finishing writing, you can preview how it will look to the user, save changes, and start the campaign by clicking on the &lt;strong&gt;Start campaign&lt;/strong&gt; button at the top.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Newsletter email received from Listmonk&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/email-received.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;And seconds later, I received this test email in my Gmail account. It&#39;s mostly plaintext and uses HTML for the hyperlinks. I&#39;ll probably use HTML for images if I add any.&lt;/p&gt;
&lt;p&gt;That&#39;s it.&lt;/p&gt;
&lt;p&gt;If you&#39;re setting Listmonk for yourself and got stuck somewhere, please email me and I would be happy to help.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Feb 4, 2026&lt;/p&gt;
&lt;p&gt;Amazon has rejected my application for SES approval without giving any proper explanations, and I received this email from them.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Amazon rejected my SES application&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/aws-ses-rejected-email.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, with SES out of the question, I evaluated other SMTP options and have settled with this service called &lt;a href=&quot;https://go.deepakness.com/smtp2go&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SMTP2GO&lt;/a&gt; for now. It offers 1,000 emails per month for free (with a limit of 200/day), and that&#39;s enough for me as of now, as I do not have any subscribers yet.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Using Listmonk with SMTP2GO&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/listmonk-setup/listmonk-with-smtp2go.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Trying and Migrating to Vivaldi Browser</title>
    <link href="https://deepakness.com/blog/vivaldi-browser/" />
    <updated>2026-02-03T00:00:00Z</updated>
    <published>2026-01-29T20:43:04Z</published>
    <id>https://deepakness.com/blog/vivaldi-browser/</id>
    <summary>My experience of trying and migrating to the Vivaldi browser - things I liked and things I didn&#39;t.</summary>
    <content type="html">&lt;p&gt;I have known the Vivaldi browser for years but never actually tried it, until it was recently recommended by &lt;a href=&quot;https://x.com/elralfchristian/status/2016924706369147388&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ralf Christian on X&lt;/a&gt;. Currently, I am using the &lt;a href=&quot;https://deepakness.com/blog/helium-browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Helium browser&lt;/a&gt; for the last few months, and it works fine but lacks some features like Widevine DRM, translation, etc.&lt;/p&gt;
&lt;p&gt;So... this time I actually decided to try the &lt;a href=&quot;https://vivaldi.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Vivaldi browser&lt;/a&gt;. I&#39;m on macOS currently, so I downloaded the &lt;code&gt;.dmg&lt;/code&gt; installation file (~220 MB in filesize) and installed it as you would any other app.&lt;/p&gt;
&lt;p&gt;In this post, I will share my experience of using the browser – things I liked and also things I didn&#39;t.&lt;/p&gt;
&lt;h2&gt;1. First impression&lt;/h2&gt;
&lt;p&gt;After opening the browser for the first time, the onboarding flow was great, and 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.&lt;/p&gt;
&lt;p&gt;It took me almost an hour to tweak small things and the final look was like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Vivaldi light mode look on my computer&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-first-look.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here are the things that I liked and didn&#39;t like in the first hour of using the browser:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Loved the different themes it provides by default and how customizable everything is.&lt;/li&gt;
&lt;li&gt;Settings window was overwhelming as it has lots of options, but it&#39;s like this for the good reasons.&lt;/li&gt;
&lt;li&gt;Loved the widgets on the home page and used the Date and Privacy Statistics ones.&lt;/li&gt;
&lt;li&gt;Break Mode (ctrl + .) is amazing.&lt;/li&gt;
&lt;li&gt;Different profiles were an expected feature, but Workspaces are an added benefit.&lt;/li&gt;
&lt;li&gt;The inbuilt screenshot and page tiling feature.&lt;/li&gt;
&lt;li&gt;Proper support for Chrome extensions, I could easily install one from Chrome webstore.&lt;/li&gt;
&lt;li&gt;Vertical tabs, I&#39;m not using it but great to have the feature.&lt;/li&gt;
&lt;li&gt;A built-in Pomodoro timer? Amazing.&lt;/li&gt;
&lt;li&gt;Support for the Manifest v2 Chrome extensions. I successfully installed this &lt;a href=&quot;https://chromewebstore.google.com/detail/scraper/mbigbapnjcgaffohmbkdlecaccepngjd&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Scraper&lt;/a&gt; extension.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt=&quot;Vivaldi settings&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-settings.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;2. Customizing as per my needs&lt;/h2&gt;
&lt;p&gt;I changed the below settings from how the browser ships with the default settings:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I don&#39;t like my tabs colors changing for different websites I visit, so I had to turn this off. But it was tricky, as searching &amp;quot;accent&amp;quot; in Settings doesn&#39;t show anything and I had to manually find this option under Themes &amp;gt; Editor &amp;gt; &lt;strong&gt;Accent from Page&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Removed the speed dials from new tabs, as I like to keep the new tab page clean. Currently, I&#39;m just showing the Date and Privacy Statistics widgets on the Home.&lt;/li&gt;
&lt;li&gt;Removed some items from the side panel, like some bookmarked websites, translation widget, some extensions that were shown, etc.&lt;/li&gt;
&lt;li&gt;Turned off the &lt;strong&gt;Use Tab Zoom&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I must have changed one or two more settings, but overall this was it.&lt;/p&gt;
&lt;h2&gt;3. Things I loved&lt;/h2&gt;
&lt;p&gt;Some features in the Vivaldi browser are amazing and not available in most other browsers I have tried so far.&lt;/p&gt;
&lt;h3&gt;Secure sync feature&lt;/h3&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;E2E sync feature in Vivaldi&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/vivaldi-browser/e2e-sync-feature.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I connected my personal browser profile to the Vivaldi browser on my Android phone, and the sync immediately started working.&lt;/p&gt;
&lt;h3&gt;Vivaldi for Android&lt;/h3&gt;
&lt;p&gt;Initially, I didn&#39;t know that Vivaldi even has a browser for Android phones as well, but I was amazed when I installed it. It didn&#39;t have as many customization options as the desktop one&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Vivaldi browser on Android&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-on-android.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;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 &lt;strong&gt;Send to Your Devices&lt;/strong&gt; feature.&lt;/p&gt;
&lt;h3&gt;RSS feed reader&lt;/h3&gt;
&lt;p&gt;When you visit a blog and it has an RSS feed, &lt;a href=&quot;https://vivaldi.com/features/feed-reader/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;you see a small feed icon&lt;/a&gt; to 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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;RSS feed support in Vivaldi&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/vivaldi-browser/rss-feed-support.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;It works much like a fully functional feed reader app. I currently use an RSS reader app called &lt;a href=&quot;https://netnewswire.com&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;NetNewsWire&lt;/a&gt;, but it&#39;s a great feature in the browser.&lt;/p&gt;
&lt;p&gt;By the way, there&#39;s also the Reader View that helps you read any articles without distractions. This feature is available in most browsers though.&lt;/p&gt;
&lt;h3&gt;Page Actions&lt;/h3&gt;
&lt;p&gt;One more crazy feature that I haven&#39;t seen in any browser is the Page Actions that lets you tweak any webpage for your advantage. Some cool actions are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter Black and White, Filter Grayscale, and even Filter Sepia&lt;/li&gt;
&lt;li&gt;Fonts Monospace to convert the fonts on the webpage to monospace&lt;/li&gt;
&lt;li&gt;Transition Removed to remove any kind of transitions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... and much more that you can learn about &lt;a href=&quot;https://help.vivaldi.com/desktop/appearance-customization/page-actions/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;from their documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Mail and Calendar clients&lt;/h3&gt;
&lt;p&gt;Vivaldi also has this stunning in-built &lt;a href=&quot;https://vivaldi.com/features/mail/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;email&lt;/a&gt; and &lt;a href=&quot;https://vivaldi.com/features/calendar/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;calendar&lt;/a&gt; clients that you can connect to your Gmail, iCloud, Outlook, or basically any account that supports IMAP or POP3.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Mail and Calendar client in Vivaldi&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-mail-calendar.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I connected my Google Workspace account just to see how it works, but I won&#39;t be using this feature as I prefer directly checking my emails via the web interface. But it&#39;s a great feature for people who do prefer an email/calendar client.&lt;/p&gt;
&lt;p&gt;Or... maybe I will use this feature after I get more comfortable with the browser.&lt;/p&gt;
&lt;h2&gt;4. Not so cool things&lt;/h2&gt;
&lt;p&gt;But like anything else in the world, nothing is perfect. I didn&#39;t like a few things in the browser:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Dragging a tab from my laptop screen to my second screen has some bugs. I tried dragging an already playing YouTube tab and then the tab was lost – it kept playing in the background but was not visible on either the first or second screen. I had to restart my browser to fix this issue.&lt;/li&gt;
&lt;li&gt;Import feature doesn&#39;t work for different Chrome browser profiles. I tried importing my main profile and it worked, but I couldn&#39;t find a way to import other Chrome profiles. But it&#39;s not a deal-breaker, at least for me.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;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&#39;s been only a few hours since I installed it, but I am loving it so far.&lt;/p&gt;
&lt;p&gt;I will keep this page updated, if I learn something new about the browser.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Jan 31, 2026&lt;/p&gt;
&lt;p&gt;I turned off the drag-n-drop tab tiling feature by disabling the &lt;strong&gt;Enable Drag and Drop Tiling&lt;/strong&gt; option under Settings &amp;gt; 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&#39;t like above).&lt;/p&gt;
&lt;p&gt;It&#39;s working fine after turning this off, and I can still tile tabs by clicking on the Page Tiling option in the bottom panel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Feb 01, 2026&lt;/p&gt;
&lt;p&gt;I modified the below options from the Settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enabled Show Full Address from Settings &amp;gt; Address Bar &amp;gt; Address Field Options, as I like seeing the full URLs at the top.&lt;/li&gt;
&lt;li&gt;Disabled Display Downloads Automatically from Settings &amp;gt; Downloads, as it was automatically opening the left side panel whenever I downloaded something.&lt;/li&gt;
&lt;li&gt;Disabled Prioritize Bookmarks from Settings &amp;gt; Address Bar &amp;gt; Address Field Suggestions, as it was always over-prioritizing bookmarks even after opening another website with a similar name multiple times.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Feb 03, 2026&lt;/p&gt;
&lt;p&gt;The #1 bug I talked about in the above &amp;quot;not so cool things&amp;quot; section has become really annoying, so I reported it to the Vivaldi team and hopefully they do something about it. I haven&#39;t received any response from them yet, but this is the bug I reported.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Vivaldi Tab dragging bug reported&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/vivaldi-browser/vivaldi-bug-reported.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Currently, I am not dragging tabs out of the current window because this is super buggy. And since dragging windows works fine, I set up a keyboard shortcut &lt;code&gt;cmd + ctrl + n&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Move a tab to a new window&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/vivaldi-browser/move-tab-to-a-new-window.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;To set this up, go to Settings &amp;gt; Keyboard, expand Tab toggle, and press whatever combination you want in front of Move Tab to New Window option.&lt;/p&gt;
&lt;p&gt;To be honest, I am liking the keyboard shortcut method much better than dragging the tab away.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Better Handling Images on My 11ty Blog</title>
    <link href="https://deepakness.com/blog/better-images/" />
    <updated>2026-01-28T00:00:00Z</updated>
    <published>2026-01-27T17:57:12Z</published>
    <id>https://deepakness.com/blog/better-images/</id>
    <summary>Better image handling on my blog and pre-optimizing them all instead of using @11ty/eleventy-img plugin.</summary>
    <content type="html">&lt;p&gt;Last year I &lt;a href=&quot;https://deepakness.com/blog/from-wordpress-to-11ty/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;migrated away from WordPress&lt;/a&gt; and I am now using 11ty SSG for my blog &lt;code&gt;deepakness.com&lt;/code&gt;. While I was mostly happy with everything, I have also occasionally &lt;a href=&quot;https://x.com/DeepakNesss/status/1994638454026899806&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;had second thoughts&lt;/a&gt; about using 11ty. The main reason was the increasing Netlify build times as my blog grew. A build was taking up to 1 min 23 seconds, and once or twice I even hit the Netlify monthly 300-minute build time limit.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Old Netlify build time&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/better-images/old-netlify-build-time.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;So I had to find a better way for the site, and I had the following two options in my mind:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ditch 11ty and start using WordPress or something similar&lt;/li&gt;
&lt;li&gt;Stay on 11ty and optimize the build time on Netlify&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I went with the #2 option as it required much less work than migrating back to WordPress, and started looking into what&#39;s causing the build times to increase so much.&lt;/p&gt;
&lt;p&gt;And it was the &lt;a href=&quot;https://www.11ty.dev/docs/plugins/image/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;@11ty/eleventy-img plugin&lt;/a&gt; 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 &lt;a href=&quot;https://github.com/11ty/demo-eleventy-img-netlify-cache/blob/main/netlify.toml&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;netlify-plugin-cache&lt;/a&gt; to cache already transformed images, but it didn&#39;t properly work because I am co-locating all images in separate blog folders, as you see below:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;blog/
└── some-post/
    ├── index.md
    ├── image-1.png
    └── image-2.png&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So... what was the solution here?&lt;/p&gt;
&lt;p&gt;I removed the &lt;code&gt;@11ty/eleventy-img&lt;/code&gt; plugin and decided to locally process all images using the &lt;code&gt;sharp&lt;/code&gt; &lt;a href=&quot;https://sharp.pixelplumbing.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;image library&lt;/a&gt;, before sending them to Netlify because the build times will be faster then.&lt;/p&gt;
&lt;p&gt;This worked as expected, and here&#39;s what I am doing with images:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Resize original images to 1600px, compress, and keep them in their original format&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;.webp&lt;/code&gt; version of images (1200px wide), and save with the same filename&lt;/li&gt;
&lt;li&gt;Track already optimized images in the &lt;code&gt;_data/cache/image-cache.json&lt;/code&gt; file via MD5 hash&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And all this happens with the following script I created:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;path&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; crypto &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;crypto&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; sharp &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;sharp&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fileURLToPath &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;url&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; __filename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fileURLToPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; __dirname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__filename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;ROOT_DIR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__dirname&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;..&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BLOG_DIR&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ROOT_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;content&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;blog&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CACHE_FILE&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ROOT_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;_data&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;cache&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;image-cache.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Configuration&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;webp&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;maxWidth&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;quality&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;original&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;maxWidth&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1600&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;extensions&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.jpg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.jpeg&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.png&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Load or create cache&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loadCache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CACHE_FILE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf-8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;saveCache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CACHE_FILE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cache&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Calculate file hash&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFileHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;filePath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; crypto&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;createHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;md5&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;hex&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Find all images in blog folders&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findImages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; images &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;existsSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BLOG_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Blog directory not found:&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BLOG_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; images&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postDirs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BLOG_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;withFileTypes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; d&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isDirectory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;BLOG_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; d&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; postDir &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; postDirs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readdirSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postDir&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; file &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; files&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ext &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;extname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toLowerCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;extensions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;.webp&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        images&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;postDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; images&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Optimize a single image&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;optimizeImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; relativePath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;relative&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ROOT_DIR&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; hash &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFileHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  
  &lt;span class=&quot;token comment&quot;&gt;// Skip if already optimized with same hash&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cache&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;relativePath&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;hash &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; hash&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;⏭️  Skipping (unchanged): &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;relativePath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dir &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ext &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;extname&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; basename &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; webpPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;basename&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.webp&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; image &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sharp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; metadata &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; image&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Create WebP version (max 1200px, quality 80)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; webpPipeline &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sharp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; finalWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; finalHeight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxWidth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ratio &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxWidth &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      finalWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxWidth&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      finalHeight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; ratio&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      webpPipeline &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; webpPipeline&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxWidth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; webpPipeline
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;webp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;quality&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;webp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;quality &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;webpPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Optimize original (max 1600px, keep format)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;original&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxWidth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; tempPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; imagePath &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.tmp&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sharp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CONFIG&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;original&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxWidth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tempPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;renameSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;tempPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Recalculate dimensions after resize&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newMeta &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sharp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      finalWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newMeta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      finalHeight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newMeta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Update cache with dimensions&lt;/span&gt;
    cache&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;relativePath&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFileHash&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Recalculate after optimization&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; finalWidth&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; finalHeight&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;optimizedAt&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toISOString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;✅ Optimized: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;relativePath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; (&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;finalWidth&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;finalHeight&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;❌ Error optimizing &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;relativePath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Main function&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&#92;n🖼️  Starting image optimization...&#92;n&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cache &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;loadCache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; images &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findImages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Found &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;images&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; images to check&#92;n&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; optimized &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; skipped &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; imagePath &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; images&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; wasOptimized &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;optimizeImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;imagePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; cache&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wasOptimized&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; optimized&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; skipped&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;saveCache&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cache&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n✨ Done! Optimized: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;optimized&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, Skipped: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;skipped&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#92;n&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script &lt;a href=&quot;https://deepakness.com/blog/optimizing-images/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;optimizes the original image&lt;/a&gt;, creates a WebP version of the image, and updates the &lt;code&gt;_data/cache/image-cache.json&lt;/code&gt; 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 &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; in the &lt;code&gt;img&lt;/code&gt; tags to avoid any &lt;a href=&quot;https://web.dev/articles/cls&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;cumulative layout shift&lt;/a&gt; on the pages.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Local image optimization using sharp library&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/better-images/local-image-optimization.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;And after this my Netlify build time was reduced to just 20 seconds, as you see in the screenshot below:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;New Netlify build time&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/better-images/new-netlify-build-time.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;And I use the HTML &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; tag to show images as you see below.&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;picture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;source&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;srcset&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;./image.webp&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;image/webp&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;img&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;alt&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;alt text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;decoding&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;async&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;1200&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;630&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;./image.png&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;picture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser first tries to load the &lt;code&gt;.webp&lt;/code&gt; version of the image, and &lt;a href=&quot;https://caniuse.com/webp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;if it&#39;s not supported&lt;/a&gt; then loads the optimized version in the original format (PNG/JPEG). I only link to the original image in my Markdown file as &lt;code&gt;![alt](./image.png)&lt;/code&gt; and the above &lt;code&gt;picture&lt;/code&gt; tag with WebP is automatically added in the final HTML via the following code in the &lt;code&gt;eleventy.config.js&lt;/code&gt; file:&lt;/p&gt;
&lt;pre class=&quot;language-javascript&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;	&lt;span class=&quot;token comment&quot;&gt;// Picture tag transform: wrap &amp;lt;img&gt; with &amp;lt;picture&gt; for WebP + fallback&lt;/span&gt;
eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addTransform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;pictureTag&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; outputPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Only apply to blog pages&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;outputPath &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.html&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;outputPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/blog/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Load image dimensions cache&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; imageCache &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;_data/cache/image-cache.json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf-8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    imageCache &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cacheContent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// Cache not found, continue without dimensions&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Extract blog slug from outputPath (e.g., &quot;_site/blog/better-images/index.html&quot; -&gt; &quot;better-images&quot;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; blogMatch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; outputPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;&#92;/blog&#92;/([^/]+)&#92;/&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; blogSlug &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; blogMatch &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; blogMatch&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Match &amp;lt;img&gt; tags with jpg/jpeg/png src&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;&amp;lt;img&#92;s+([^&gt;]*?)src=[&quot;&#39;]([^&quot;&#39;]+&#92;.(jpg|jpeg|png))[&quot;&#39;]([^&gt;]*)&gt;&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;gi&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;match&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; before&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; src&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; after&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Skip if already inside a &amp;lt;picture&gt; tag&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;match&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;data-no-picture&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// Create WebP source path&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; webpSrc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; src&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;&#92;.(jpg|jpeg|png)$&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;i&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;.webp&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// Keep existing attributes, add decoding async if not present&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; attrs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; before &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; after&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;attrs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;decoding=&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        attrs &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39; decoding=&quot;async&quot;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// Add width/height from cache if available&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Convert relative src (e.g., &quot;./image.png&quot;) to cache key (e.g., &quot;content/blog/slug/image.png&quot;)&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;blogSlug &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;attrs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;width=&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; imageName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; src&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;^&#92;.&#92;/&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Remove leading ./&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cacheKey &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;content/blog/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;blogSlug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;imageName&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cached &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; imageCache&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;cacheKey&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cached&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;width &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; cached&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          attrs &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; width=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;cached&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;width&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot; height=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;cached&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;height&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&amp;lt;picture&gt;
      &amp;lt;source srcset=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;webpSrc&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot; type=&quot;image/webp&quot;&gt;
      &amp;lt;img &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;attrs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; src=&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;src&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&gt;
    &amp;lt;/picture&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The setup is a bit complicated, but I am very much satisfied with the setup, and will stay with 11ty, for now.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Creating My First WordPress Plugin</title>
    <link href="https://deepakness.com/blog/first-wordpress-plugin/" />
    <updated>2026-01-22T00:00:00Z</updated>
    <published>2025-12-21T17:10:54Z</published>
    <id>https://deepakness.com/blog/first-wordpress-plugin/</id>
    <summary>I created my very first WordPress plugin, and now it&#39;s live on the WordPress.org repository.</summary>
    <content type="html">&lt;p&gt;I created my very first &lt;a href=&quot;https://wordpress.org/plugins/bulk-classic-to-block/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;WordPress plugin called Bulk Classic to Block&lt;/a&gt;, and now it&#39;s live on the WordPress.org repository. It was a fun process, but definitely wasn&#39;t straightforward as it took me almost a month to get the &lt;code&gt;v1.0.0&lt;/code&gt; live.&lt;/p&gt;
&lt;p&gt;But I&#39;m ready to explain the entire thing in detail in this blog post, so let&#39;s directly get to it.&lt;/p&gt;
&lt;h2&gt;1. The idea for the plugin&lt;/h2&gt;
&lt;p&gt;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&#39;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.&lt;/p&gt;
&lt;p&gt;Initially, I didn&#39;t know if such a thing would even work or not, but I researched a bit and then started working on the plugin. I used Cursor AI and Google&#39;s Antigravity for developing this.&lt;/p&gt;
&lt;h2&gt;2. Initial submission to WordPress.org&lt;/h2&gt;
&lt;p&gt;I, first, submitted the plugin to &lt;a href=&quot;https://wordpress.org/plugins/developers/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;WordPress.org&lt;/a&gt; on November 29, 2025 and received the following email immediately after that.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Initial submission of the WordPress plugin&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/first-wordpress-plugin/initial-submission.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;And then I heard nothing from them for a few days.&lt;/p&gt;
&lt;h2&gt;3. Plugin checked by a volunteer&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;WordPress plugin checked by a volunteer&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/first-wordpress-plugin/plugin-checked-by-volunteer.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Mainly, the issues were related to the following 2 things (along with the solutions suggested):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use wp_enqueue commands
&lt;ul&gt;
&lt;li&gt;wp_register_script() and wp_enqueue_script() to add JavaScript code from a file.&lt;/li&gt;
&lt;li&gt;wp_add_inline_script() to add inline JavaScript code to previous declared scripts.&lt;/li&gt;
&lt;li&gt;wp_register_style() and wp_enqueue_style() to add CSS from a file.&lt;/li&gt;
&lt;li&gt;wp_add_inline_style() to add inline CSS to previously declared CSS.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Nonces and User Permissions Needed for Security
&lt;ul&gt;
&lt;li&gt;Please add a nonce check to your input calls ($_POST, $_GET, $REQUEST) to prevent unauthorized access.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. Updated and re-submitted the plugin&lt;/h2&gt;
&lt;p&gt;I then updated the plugin according to the volunteer&#39;s detailed suggestions and re-submitted it for approval on the same day. I also sent them this email, as suggested in the previous email.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Updated the WordPress plugin and sent this confirmation email&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/first-wordpress-plugin/updated-the-plugin.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;And then I waited, expecting more issues to be flagged by the volunteer.&lt;/p&gt;
&lt;h2&gt;5. The plugin gets approved&lt;/h2&gt;
&lt;p&gt;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 &lt;code&gt;https://wordpress.org/plugins/bulk-classic-to-block/&lt;/code&gt; showed nothing.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Bulk Classic to Block plugin gets approved on WordPress.org&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/first-wordpress-plugin/plugin-gets-approved.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Uploading plugin to WordPress Plugin Directory via SVN&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/first-wordpress-plugin/uploading-the-plugin-via-svn.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I started learning about &lt;a href=&quot;https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;using SVN for WordPress&lt;/a&gt; and then got some understanding that it&#39;s a version control system, much like &lt;a href=&quot;https://git-scm.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Git&lt;/a&gt;. Also learned that it can be used via CLI as well as via some GUI clients like &lt;a href=&quot;https://tortoisesvn.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;TortoiseSVN&lt;/a&gt; and &lt;a href=&quot;https://www.smartsvn.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SmartSVN&lt;/a&gt;, again, much like Git.&lt;/p&gt;
&lt;h2&gt;6. Uploading the plugin&lt;/h2&gt;
&lt;p&gt;I decided to use the CLI for using &lt;code&gt;svn&lt;/code&gt; (Subversion) because what&#39;s even the fun in using this via the GUI. At first, I had to install this on my computer. I was using macOS, so I decided to install &lt;code&gt;svn&lt;/code&gt; via &lt;a href=&quot;https://formulae.brew.sh/formula/subversion&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Homebrew&lt;/a&gt; by running the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; subversion&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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 &lt;code&gt;trunk&lt;/code&gt; folder and then screenshots and icons went in the &lt;code&gt;assets&lt;/code&gt; folder. Below are the &lt;code&gt;svn&lt;/code&gt; commands I had to execute:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Executed at first&lt;/span&gt;
svn checkout https://plugins.svn.wordpress.org/bulk-classic-to-block

&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; bulk-classic-to-block

&lt;span class=&quot;token comment&quot;&gt;# After copying files to `trunk` and `assets` folders as specified above&lt;/span&gt;
svn status

svn &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; trunk/* assets/* &lt;span class=&quot;token parameter variable&quot;&gt;--force&lt;/span&gt;

svn commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Initial commit of plugin files&quot;&lt;/span&gt;

svn copy trunk tags/1.0.0

svn commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Tag version 1.0.0&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&#39;d like, here&#39;s the entire text copied from my terminal.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;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 &lt;span class=&quot;token number&quot;&gt;3424796&lt;/span&gt;.
deepak@m2air wordpress-org % &lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; bulk-classic-to-block 
deepak@m2air bulk-classic-to-block % &lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt;
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 &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; trunk/* assets/* &lt;span class=&quot;token parameter variable&quot;&gt;--force&lt;/span&gt;
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  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  assets/screenshot-1.png
A  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  assets/screenshot-2.png
A  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  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 &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Initial commit of plugin files&quot;&lt;/span&gt;
Authentication realm: &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;https://plugins.svn.wordpress.org:44&lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;3&lt;/span&gt;&gt;&lt;/span&gt; Use your WordPress.org login
Password &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;deepak&#39;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; ********************************************

Authentication realm: &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;https://plugins.svn.wordpress.org:44&lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;3&lt;/span&gt;&gt;&lt;/span&gt; Use your WordPress.org login
Username: deepakness
Password &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;deepakness&#39;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;:&lt;/span&gt; ********************************************

Adding  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  assets/screenshot-1.png
Adding  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  assets/screenshot-2.png
Adding  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  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 &lt;span class=&quot;token function&quot;&gt;file&lt;/span&gt; data &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;done
Committing transaction&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
Committed revision &lt;span class=&quot;token number&quot;&gt;3424811&lt;/span&gt;.
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 &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Tag version 1.0.0&quot;&lt;/span&gt;
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&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
Committed revision &lt;span class=&quot;token number&quot;&gt;3424816&lt;/span&gt;.
deepak@m2air bulk-classic-to-block %&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then my WordPress plugin &lt;strong&gt;Bulk Classic to Block&lt;/strong&gt; was live, and I could access it from this URL: &lt;a href=&quot;https://wordpress.org/plugins/bulk-classic-to-block/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;https://wordpress.org/plugins/bulk-classic-to-block/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;7. Future updates&lt;/h2&gt;
&lt;p&gt;If I update anything in the plugin files in the future, I might need to run the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; ~/bulk-classic-to-block

&lt;span class=&quot;token comment&quot;&gt;# copy or edit files&lt;/span&gt;
svn &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; assets/screenshot-4.png
&lt;span class=&quot;token comment&quot;&gt;# or edit readme.txt&lt;/span&gt;

svn commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Update screenshots and readme&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Above is when I add a screenshot or a minor change without the need to bump the version, but when I have to do a new release then the process will be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make changes in files in the /trunk folder&lt;/li&gt;
&lt;li&gt;Bump the version number in:
&lt;ol&gt;
&lt;li&gt;Main plugin file&lt;/li&gt;
&lt;li&gt;readme.txt file&lt;/li&gt;
&lt;li&gt;Changelog section&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;Commit /trunk by using the &lt;code&gt;svn commit -m &amp;quot;message&amp;quot;&lt;/code&gt; command&lt;/li&gt;
&lt;li&gt;Create a new tag (important) by running the following commands:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;svn copy trunk tags/1.0.1
svn commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Tag 1.0.1&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, some other helpful &lt;code&gt;svn&lt;/code&gt; commands can be:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# See what changed&lt;/span&gt;
svn status

&lt;span class=&quot;token comment&quot;&gt;# See diff&lt;/span&gt;
svn &lt;span class=&quot;token function&quot;&gt;diff&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Update local copy&lt;/span&gt;
svn update&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it!&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update: 22 Jan, 2026&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I made the plugin&#39;s GitHub repository open source; &lt;a href=&quot;https://github.com/deepakness/wp-bulk-classic-to-block&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;you can access it here&lt;/a&gt;. Now, you can either download the plugin from &lt;a href=&quot;https://github.com/deepakness/wp-bulk-classic-to-block/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;GitHub releases&lt;/a&gt; or directly install via the &lt;a href=&quot;https://wordpress.org/plugins/bulk-classic-to-block/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;official WordPress.org repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Will keep adding plugin updates to this page in the future.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Upgrading Self-hosted Umami to v3 on a VPS</title>
    <link href="https://deepakness.com/blog/upgrading-umami-v3/" />
    <updated>2025-11-19T17:24:51Z</updated>
    <id>https://deepakness.com/blog/upgrading-umami-v3/</id>
    <summary>A quick guide to upgrading self-hosted Umami to the latest v3 on a Hetzner VPS.</summary>
    <content type="html">&lt;p&gt;I recently wrote about &lt;a href=&quot;https://deepakness.com/blog/self-hosting-umami-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;self-hosting Umami Analytics&lt;/a&gt; on a small Hetzner VPS using Docker and Caddy.&lt;/p&gt;
&lt;p&gt;Since then, &lt;a href=&quot;https://umami.is/blog/umami-v3&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Umami shipped v3&lt;/a&gt; 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.&lt;/p&gt;
&lt;p&gt;If you installed Umami by following &lt;a href=&quot;https://deepakness.com/blog/self-hosting-umami-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my earlier post&lt;/a&gt;, this guide should be a direct drop-in upgrade for you.&lt;/p&gt;
&lt;h2&gt;1. Log in to the VPS&lt;/h2&gt;
&lt;p&gt;From my local machine (macOS), I first SSHed into the VPS:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; root@your_server_ip&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server is running Ubuntu 24.04 with Docker CE, same as before.&lt;/p&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;reboot&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I connected again:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; root@your_server_ip&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2. Confirm the current Umami setup&lt;/h2&gt;
&lt;p&gt;Once logged in, I checked what files are in the root directory:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Caddyfile  docker-compose.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I opened the &lt;code&gt;docker-compose.yml&lt;/code&gt; to confirm the current setup:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; docker-compose.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Umami Docker Compose file contents&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/upgrading-umami-v3/umami-docker-compose-file.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is the same Docker setup I used in the original installation post:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;umami&lt;/code&gt; container using &lt;code&gt;postgresql-latest&lt;/code&gt; image&lt;/li&gt;
&lt;li&gt;&lt;code&gt;db&lt;/code&gt; container running &lt;code&gt;postgres:15-alpine&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;caddy&lt;/code&gt; in front, handling HTTPS and the domain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To make sure everything was running fine before touching anything, I checked the containers:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose &lt;span class=&quot;token function&quot;&gt;ps&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sample output:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;NAME           IMAGE                                            COMMAND                  SERVICE   CREATED       STATUS                 PORTS
root-caddy-1   caddy:latest                                     &quot;caddy run --config …&quot;   caddy     4 weeks ago   Up 2 hours             0.0.0.0:80-&gt;80/tcp, ...
root-db-1      postgres:15-alpine                               &quot;docker-entrypoint.s…&quot;   db        4 weeks ago   Up 2 hours (healthy)   5432/tcp
root-umami-1   ghcr.io/umami-software/umami:postgresql-latest   &quot;docker-entrypoint.s…&quot;   umami     4 weeks ago   Up 2 hours (healthy)   3000/tcp&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All good here.&lt;/p&gt;
&lt;h2&gt;3. Create a PostgreSQL backup (do not skip this)&lt;/h2&gt;
&lt;p&gt;Before changing any image or running migrations, I created a full dump of the Umami database.&lt;/p&gt;
&lt;p&gt;Since the &lt;code&gt;db&lt;/code&gt; service is using:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POSTGRES_DB=umami&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POSTGRES_USER=umami&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;the backup command is:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose &lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; db pg_dump &lt;span class=&quot;token parameter variable&quot;&gt;-U&lt;/span&gt; umami umami &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; umami-backup-&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;date&lt;/span&gt; +%F-%H%M&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs &lt;code&gt;pg_dump&lt;/code&gt; inside the &lt;code&gt;db&lt;/code&gt; container and saves the file on the host with a timestamp in the name.&lt;/p&gt;
&lt;p&gt;Then I double checked that the backup file exists and has some size:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-lh&lt;/span&gt; umami-backup-*.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output on my server:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;-rw-r--r-- 1 root root 8.5M Nov 19 14:48 umami-backup-2025-11-19-1448.sql&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;8.5 MB looks right for my current traffic.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;4. Update the Docker image to Umami v3.0.1&lt;/h2&gt;
&lt;p&gt;Next, I edited &lt;code&gt;docker-compose.yml&lt;/code&gt; to point to the new v3 image.&lt;/p&gt;
&lt;p&gt;I used &lt;code&gt;nano&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;nano&lt;/span&gt; docker-compose.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Inside the file, in the &lt;code&gt;umami&lt;/code&gt; service, &lt;strong&gt;I changed this line&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/umami&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;software/umami&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;postgresql&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;to this&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/umami&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;software/umami&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;3.0.1&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Nothing else needed to change – the &lt;code&gt;DATABASE_URL&lt;/code&gt;, &lt;code&gt;DATABASE_TYPE&lt;/code&gt;, &lt;code&gt;APP_SECRET&lt;/code&gt;, and the &lt;code&gt;db&lt;/code&gt; service all stay the same.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After saving and closing nano (&lt;code&gt;Ctrl+X&lt;/code&gt;, &lt;code&gt;Y&lt;/code&gt;, &lt;code&gt;Enter&lt;/code&gt;), I confirmed the change with:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A3&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;umami:&#39;&lt;/span&gt; docker-compose.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;  umami:
    image: ghcr.io/umami-software/umami:3.0.1
    environment:
      DATABASE_URL: postgresql://umami:umami@db:5432/umami&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Perfect.&lt;/p&gt;
&lt;h2&gt;5. Pull the new image&lt;/h2&gt;
&lt;p&gt;Now that the compose file points to &lt;code&gt;3.0.1&lt;/code&gt;, I pulled the new image:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose pull umami&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This downloads the &lt;code&gt;ghcr.io/umami-software/umami:3.0.1&lt;/code&gt; image. My output looked like this (trimmed):&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[+] Pulling 18/18
 ✔ umami Pulled                                                                       16.0s 
   ✔ fc2cca81d0de Pull complete                                                        2.4s 
   ✔ ...
   ✔ 14e4cc53c863 Pull complete                                                       14.8s &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once this finished, the image was ready on the server.&lt;/p&gt;
&lt;h2&gt;6. Recreate the Umami container with v3&lt;/h2&gt;
&lt;p&gt;With the image pulled, I restarted just the &lt;code&gt;umami&lt;/code&gt; service using the new image:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; --force-recreate umami&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[+] Running 2/2
 ✔ Container root-db-1     Healthy                                                     0.8s 
 ✔ Container root-umami-1  Started                                                     1.1s &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the &lt;code&gt;db&lt;/code&gt; container stays the same, including the data volume. We are only replacing the app container.&lt;/p&gt;
&lt;h2&gt;7. Watch the logs and wait for migrations&lt;/h2&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose logs &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; umami&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The logs showed:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;umami-1  | &gt; umami@3.0.1 start-docker /app
umami-1  | &gt; npm-run-all check-db update-tracker start-server
umami-1  | 
umami-1  | &gt; umami@3.0.1 check-db /app
umami-1  | &gt; 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 &quot;db&quot;: PostgreSQL database &quot;umami&quot;, schema &quot;public&quot; at &quot;db:5432&quot;
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  | &gt; umami@3.0.1 start-server /app
umami-1  | &gt; 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&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is exactly what you want to see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DB connection successful&lt;/li&gt;
&lt;li&gt;Migrations applied&lt;/li&gt;
&lt;li&gt;Server starting and ready&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once I saw the app was ready, I pressed &lt;code&gt;Ctrl + C&lt;/code&gt; to stop tailing the logs.&lt;/p&gt;
&lt;h2&gt;8. Confirm that Umami v3 is live&lt;/h2&gt;
&lt;p&gt;With the container running, I opened my Umami domain in the browser (the one I pointed to Caddy in the first post).&lt;/p&gt;
&lt;p&gt;The login page loaded as usual. After logging in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The dashboard still showed all the historical data&lt;/li&gt;
&lt;li&gt;The list of websites was intact&lt;/li&gt;
&lt;li&gt;New pageviews were being tracked without changing the tracking script&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;9. What to do if something breaks&lt;/h2&gt;
&lt;p&gt;If the logs show errors, or the app keeps restarting in a loop, here is a quick recovery plan:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Stop the stack:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose down&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bring only the database back up:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; db&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Restore the backup:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; umami-backup-YYYY-MM-DD-HHMM.sql &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose &lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-T&lt;/span&gt; db psql &lt;span class=&quot;token parameter variable&quot;&gt;-U&lt;/span&gt; umami umami&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the file name with your backup file name.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Change the image back to the old one in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/umami&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;software/umami&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;postgresql&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Start Umami again:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; umami&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You should now be back on the old version with the old data.&lt;/p&gt;
&lt;h2&gt;10. Final notes&lt;/h2&gt;
&lt;p&gt;That is pretty much all I had to do to upgrade Umami from the &lt;code&gt;postgresql-latest&lt;/code&gt; v2 image to &lt;code&gt;3.0.1&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One SSH session&lt;/li&gt;
&lt;li&gt;One &lt;code&gt;pg_dump&lt;/code&gt; backup&lt;/li&gt;
&lt;li&gt;One image tag change&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;pull&lt;/code&gt;, an &lt;code&gt;up&lt;/code&gt;, and a quick log check&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you are running the same Docker + Caddy setup &lt;a href=&quot;https://deepakness.com/blog/self-hosting-umami-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;from my earlier post&lt;/a&gt;, you can follow the same steps line by line, just replace the IP and domain with yours.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Self-hosting Umami Analytics on a VPS</title>
    <link href="https://deepakness.com/blog/self-hosting-umami-analytics/" />
    <updated>2025-11-19T00:00:00Z</updated>
    <published>2025-10-20T02:58:00Z</published>
    <id>https://deepakness.com/blog/self-hosting-umami-analytics/</id>
    <summary>A quick guide to self-hosting Umami Analytics on a $3.50 Hetzner VPS via Docker.</summary>
    <content type="html">&lt;p&gt;A few days ago, I &lt;a href=&quot;https://deepakness.com/blog/self-hosting-plausible-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;self-hosted Plausible analytics&lt;/a&gt; and &lt;a href=&quot;https://x.com/DeepakNesss/status/1979566150335008797&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;many people recommended&lt;/a&gt; that I should check Umami analytics as it&#39;s lightweight and updates are more frequent.&lt;/p&gt;
&lt;p&gt;So here it is... I also self-hosted &lt;a href=&quot;https://github.com/umami-software/umami/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Umami analytics&lt;/a&gt; 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.&lt;/p&gt;
&lt;h2&gt;Installing Umami on a Hetzner VPS&lt;/h2&gt;
&lt;p&gt;First, you need to have a &lt;a href=&quot;https://go.deepakness.com/hetzner&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Hetzner account&lt;/a&gt; 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).&lt;/p&gt;
&lt;h3&gt;1. Set up the VPS&lt;/h3&gt;
&lt;p&gt;Create a new server, even the smallest shared server is enough to host Umami as &lt;a href=&quot;https://lowendtalk.com/discussion/comment/4536502/#Comment_4536502&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;people discussed&lt;/a&gt; that only 500 MB RAM is enough to efficiently run it.&lt;/p&gt;
&lt;p&gt;Also, I&#39;m not going to explain how to set up a new server on Hetzner so if you need help with that you can &lt;a href=&quot;https://deepakness.com/blog/self-hosting-plausible-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;go through this post&lt;/a&gt;, as I have explained the process a bit.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;And as explained, &lt;strong&gt;set up a Docker CE app&lt;/strong&gt; instead of setting up a blank system as we&#39;ll be installing Umami via Docker.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, connect to the server via SSH from your terminal by running the &lt;code&gt;ssh root@your_ip_address&lt;/code&gt; command, and once connected, update the system by running the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; upgrade&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it&#39;s ready for the installation.&lt;/p&gt;
&lt;h3&gt;2. Umami and Caddy setup via Docker&lt;/h3&gt;
&lt;p&gt;While you can directly follow the official &lt;a href=&quot;https://github.com/umami-software/umami/?tab=readme-ov-file#-installing-with-docker&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;installing with Docker&lt;/a&gt; steps, I have a better way of doing it by also using Caddy to set up the custom domain.&lt;/p&gt;
&lt;p&gt;For this, you need to create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file in the root by running the following &lt;code&gt;nano&lt;/code&gt; command.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;nano&lt;/span&gt; docker-compose.yml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, just copy-paste the below YML code in the file, then save and exit by pressing &lt;code&gt;ctrl + x&lt;/code&gt; and then &lt;code&gt;y&lt;/code&gt;. But replace &lt;code&gt;replace-me-with-a-random-string&lt;/code&gt; with a string before pasting; I prefer running below openssl command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;openssl rand &lt;span class=&quot;token parameter variable&quot;&gt;-base64&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;32&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then use the generated string below:&lt;/p&gt;
&lt;pre class=&quot;language-yml&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-yml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;umami&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ghcr.io/umami&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;software/umami&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;postgresql&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;DATABASE_URL&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgresql&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//umami&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;umami@db&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;5432/umami
      &lt;span class=&quot;token key atrule&quot;&gt;DATABASE_TYPE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgresql
      &lt;span class=&quot;token key atrule&quot;&gt;APP_SECRET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; to4KhI0g8Wzaw1r7+vEknSbU4AM4d6LRJyGzlgO5DZY=
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service_healthy
    &lt;span class=&quot;token key atrule&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always
    &lt;span class=&quot;token key atrule&quot;&gt;healthcheck&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CMD-SHELL&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;curl http://localhost:3000/api/heartbeat&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;15&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_DB&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; umami
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_USER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; umami
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; umami
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; umami&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;db&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/var/lib/postgresql/data
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; always
    &lt;span class=&quot;token key atrule&quot;&gt;healthcheck&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CMD-SHELL&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;caddy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caddy&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;80:80&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;443:443&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;443:443/udp&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;umami&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service_started
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./Caddyfile&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/etc/caddy/Caddyfile&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;ro
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; caddy_data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/data
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; caddy_config&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/config

&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;umami-db-data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;caddy_data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;caddy_config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, create a &lt;code&gt;Caddyfile&lt;/code&gt; by running the below command and paste the below Caddyfile code, but first replace the &lt;code&gt;your_domain_or_subdomain_here&lt;/code&gt; with your domain/subdomain.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;nano&lt;/span&gt; Caddyfile&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, I can connect it to &lt;code&gt;umami.deepakness.com&lt;/code&gt;. Again, press &lt;code&gt;ctrl + x&lt;/code&gt; and then &lt;code&gt;y&lt;/code&gt; to save and exit.&lt;/p&gt;
&lt;pre class=&quot;language-caddyfile&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-caddyfile&quot;&gt;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
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, you will need to add a DNS record at Cloudflare (if you&#39;re using it) or at your domain registrar. You need to add an &lt;code&gt;A&lt;/code&gt; record with your IP address.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Adding a DNS record in Cloudflare.&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-umami-analytics/cloudflare-dns-record.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;For example, I use Cloudflare and this is how I&#39;d add the &lt;code&gt;A&lt;/code&gt; DNS record in there.&lt;/p&gt;
&lt;h3&gt;3. Start Docker containers&lt;/h3&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose logs &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; caddy&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And your Umami Analytics instance should be live!&lt;/p&gt;
&lt;p&gt;You can visit the domain/subdomain, and you should see the login screen. Your default username will be &lt;code&gt;admin&lt;/code&gt;, and your password will be &lt;code&gt;umami&lt;/code&gt;. But you should &lt;a href=&quot;https://umami.is/docs/login&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;immediately change your password&lt;/a&gt; after logging in.&lt;/p&gt;
&lt;p&gt;And that&#39;s it.&lt;/p&gt;
&lt;p&gt;You can add your website(s) for tracking and &lt;a href=&quot;https://umami.is/docs/collect-data&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;start collecting data&lt;/a&gt; as explained in their docs.&lt;/p&gt;
&lt;h2&gt;My experience with Umami&lt;/h2&gt;
&lt;p&gt;Actually, I &lt;a href=&quot;https://x.com/DeepakNesss/status/1979566150335008797&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;liked Umami a lot&lt;/a&gt;, it&#39;s better than Plausible in terms of server resource usage. You can see the usage here for both:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plausible is using 1 GB of RAM:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Plausible htop RAM usage&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-umami-analytics/plausible-htop.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Umami is using ~500 MB of RAM:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Umami htop RAM usage&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-umami-analytics/umami-htop.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Apart from this, I also liked the Umami dashboard a lot as it shows more tracking data while consuming 50% fewer resources than Plausible. Also, Plausible keeps showing a huge footer like below on all pages which I didn&#39;t like, but it&#39;s not there in Umami.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Unnecessary Plausible footer&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-umami-analytics/plausible-footer.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Apart from this, I liked the data points being shown in Umami, especially the way sessions and events data is shown.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Sessions data in Umami&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-umami-analytics/umami-sessions.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;So... that&#39;s it.&lt;/p&gt;
&lt;p&gt;This time, I am going with Umami as it seemed more polished to me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update: November 19, 2025&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I upgraded Umami to v3.0.1 and it was a smooth process. Check out &lt;a href=&quot;https://deepakness.com/blog/upgrading-umami-v3&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my post on upgrading Umami to v3&lt;/a&gt; for more details. I have explained the process in detail.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Self-hosting Plausible on a Hetzner VPS</title>
    <link href="https://deepakness.com/blog/self-hosting-plausible-analytics/" />
    <updated>2025-10-18T13:19:01Z</updated>
    <id>https://deepakness.com/blog/self-hosting-plausible-analytics/</id>
    <summary>A quick guide to self-hosting Plausible Community Edition on a 3.50 USD Hetzner VPS.</summary>
    <content type="html">&lt;p&gt;Previously, I have always used Google Analytics to track visitors on my website, but this time, I decided to use Plausible analytics on &lt;a href=&quot;https://deepakness.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;my personal website&lt;/a&gt;. And mainly because of the following three reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Plausible is lightweight, so it doesn&#39;t affect the website loading speed much&lt;/li&gt;
&lt;li&gt;Unlike Google Analytics, Plausible is open-source and privacy-friendly, and&lt;/li&gt;
&lt;li&gt;The Plausible dashboard is user-friendly and shows essential data without complexity&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, let&#39;s directly go to the self-hosting part. I will explain each and every step of self-hosting &lt;a href=&quot;https://github.com/plausible/community-edition/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Plausible CE&lt;/a&gt; on a Hetzner VPS.&lt;/p&gt;
&lt;h2&gt;1. Set up a Hetzner VPS&lt;/h2&gt;
&lt;p&gt;If you don&#39;t have a &lt;a href=&quot;https://go.deepakness.com/hetzner&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Hetzner account&lt;/a&gt;, create one and get yourself verified by submitting an ID proof (I don&#39;t know if that&#39;s for everyone, but I did have to submit). After logging in, go to &lt;code&gt;console.hetzner.com&lt;/code&gt; page, create a Project and then go inside the Project to create a Server.&lt;/p&gt;
&lt;p&gt;Click on the big &lt;strong&gt;Add Server&lt;/strong&gt; button, and you will land on the below page. In this case, select the &lt;strong&gt;Cost-Optimized x86 (Intel®/AMD)&lt;/strong&gt; version and then the &lt;strong&gt;CX23&lt;/strong&gt; server as shown in the screenshot. And this is more than enough to run Plausible, as it only &lt;a href=&quot;https://github.com/plausible/community-edition/?tab=readme-ov-file#prerequisites&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;requires 2GB of RAM&lt;/a&gt; to run efficiently, and we have 4GB here.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Create a new server on Hetzner&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-plausible-analytics/hetzner-create-server.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;Below are the recommendations for other options on the page:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Location&lt;/strong&gt;: Leave the location to Helsinki as cheaper servers are available here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Image&lt;/strong&gt;: Instead of choosing OS Images, go to the &lt;strong&gt;Apps&lt;/strong&gt; section and choose &lt;strong&gt;Docker CE&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Networking&lt;/strong&gt;: Make sure IPv4 and IPv6 are selected. They should already be selected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSH Keys&lt;/strong&gt;: If you use SSH keys on your computer, I&#39;d recommend using these for easier access later. Maybe look it up online on how to set it up for your OS.&lt;/li&gt;
&lt;li&gt;Leave Volumes, Firewalls, Backups, Placement Groups, Labels, and Cloud config as they are. No need to write anything here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;: Give your server a recognizable name for future reference. You can always change it later.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Click on the &lt;strong&gt;Create and Buy Now&lt;/strong&gt; button and your server will be ready in a few minutes.&lt;/p&gt;
&lt;h2&gt;2. Install Plausible on the server&lt;/h2&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;ssh&lt;/span&gt; root@YOUR_IPv4_ADDRESS&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have the server connected via SSH, you just have to press Enter again, and it will be connected. Otherwise, you might need to enter the server root password.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you don&#39;t choose the SSH method, you will receive the root password via email.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After your terminal gets connected to the server, you can update the server by running the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; upgrade&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It should look like below:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Update and upgrade the Hetzner server&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-plausible-analytics/update-hetzner-server.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;After this completes, go to the &lt;a href=&quot;https://github.com/plausible/community-edition/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Plausible community edition GitHub repo&lt;/a&gt; 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):&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone &lt;span class=&quot;token parameter variable&quot;&gt;-b&lt;/span&gt; v3.0.1 --single-branch https://github.com/plausible/community-edition plausible-ce&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, go to the &lt;code&gt;plausible-ce&lt;/code&gt; folder by running below command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; plausible-ce&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file by running the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;touch&lt;/span&gt; .env&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then add &lt;code&gt;BASE_URL&lt;/code&gt; and &lt;code&gt;SECRET_KEY_BASE&lt;/code&gt; by running the following commands:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;BASE_URL=https://plausible.example.com&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; .env&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;SECRET_KEY_BASE=&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;openssl rand &lt;span class=&quot;token parameter variable&quot;&gt;-base64&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;48&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; .env&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make sure to replace &lt;code&gt;BASE_URL&lt;/code&gt; with the URL where you want to host Plausible. You will also need to add an &lt;code&gt;A&lt;/code&gt; DNS record with your server IP at Cloudflare (if you use Cloudflare) or at your domain registrar.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also manually add &lt;code&gt;BASE_URL&lt;/code&gt; and &lt;code&gt;SECRET_KEY_BASE&lt;/code&gt; by running the &lt;code&gt;nano .env&lt;/code&gt; command in the terminal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, run the below commands one by one:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;HTTP_PORT=80&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; .env&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;HTTPS_PORT=443&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; .env&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then run the following command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; compose.override.yml &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;EOF
services:
  plausible:
    ports:
      - 80:80
      - 443:443
EOF&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, it&#39;s time to start the services with Docker Compose by running the below command:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you will see something like below in the terminal:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Start Docker services for Plausible&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-plausible-analytics/start-docker-services.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, it&#39;s time to create a user and add your website for tracking.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I have created a short &lt;a href=&quot;https://www.youtube.com/watch?v=efPDmAiYMKY&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;video for self-hosting Umami&lt;/a&gt;, so some steps will be the same when installing Plausible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;3. Start tracking visitors&lt;/h2&gt;
&lt;p&gt;First, you will need to create a user by visiting the BASE_URL you set earlier, something like &lt;code&gt;https://plausible.example.com&lt;/code&gt; and create an account with your credentials.&lt;/p&gt;
&lt;p&gt;And when asked, add the domain you want to track, something like &lt;code&gt;deepakness.com&lt;/code&gt;, and go to the next step. You can enable optional measurements as shown below, and then &lt;strong&gt;Copy&lt;/strong&gt; the script that&#39;s being shown in the box.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Adding a new site to Plausible&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/self-hosting-plausible-analytics/add-new-site-plausible.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, go to your website and add the script somewhere between the &lt;code&gt;&amp;lt;head&amp;gt;...&amp;lt;/head&amp;gt;&lt;/code&gt; tags on your website, &lt;a href=&quot;https://plausible.io/docs/plausible-script&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;as explained in their docs&lt;/a&gt;. If you&#39;re having difficulties, you can also &lt;a href=&quot;https://plausible.io/docs/integration-guides&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;check this page&lt;/a&gt; for different installation options for websites with different tech-stacks.&lt;/p&gt;
&lt;p&gt;Lastly, click on the &lt;strong&gt;Start collecting data&lt;/strong&gt; button, and you should be good to go within a few minutes. Once correctly installed, you&#39;ll start seeing the data being shown in the dashboard.&lt;/p&gt;
&lt;p&gt;And the best thing is, you can track users on multiple websites from the single Plausible dashboard.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;While I have provided all the commands here in the post, I recommend checking on the &lt;a href=&quot;https://github.com/plausible/community-edition&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Plausible community edition repo&lt;/a&gt; in case something changes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&#39;s it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I also &lt;a href=&quot;https://deepakness.com/blog/self-hosting-umami-analytics&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;self-hosted Umami analytics&lt;/a&gt; as it was recommended by a lot of folks. In the post, you can also see a short comparison between the two.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hope this helps.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Trying and Migrating to the Helium Browser</title>
    <link href="https://deepakness.com/blog/helium-browser/" />
    <updated>2026-01-30T00:00:00Z</updated>
    <published>2025-10-03T02:08:51Z</published>
    <id>https://deepakness.com/blog/helium-browser/</id>
    <summary>My experience of completely migrating to the Helium browser, from Brave browser.</summary>
    <content type="html">&lt;p&gt;I was using the Brave browser for a few years now, but was also trying different browsers like &lt;a href=&quot;https://deepakness.com/blog/trying-ungoogled-chromium/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ungoogled Chromium&lt;/a&gt; and &lt;a href=&quot;https://deepakness.com/blog/trying-librewolf-browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;LibreWolf&lt;/a&gt; in the search of something better. Honestly, I didn&#39;t know what I was looking for in my web browser, but I just kept trying them.&lt;/p&gt;
&lt;p&gt;However, I kept coming back to the Brave browser as it has everything that others lack.&lt;/p&gt;
&lt;p&gt;But this time, I have completely migrated to the &lt;a href=&quot;https://helium.computer/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Helium browser&lt;/a&gt;, and for good. The browser was recommended to me &lt;a href=&quot;https://x.com/fardeenxyz/status/1971179056352932029&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;by Fardeen on X&lt;/a&gt;, and I &lt;a href=&quot;https://x.com/DeepakNesss/status/1971526673498833244&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;installed it the next day&lt;/a&gt; after I saw multiple people mentioning online.&lt;/p&gt;
&lt;p&gt;It&#39;s been over a week since I started using the &lt;a href=&quot;https://github.com/imputnet/helium&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;open-source Helium&lt;/a&gt; as my default browser now, and I&#39;ve never been happier using a &lt;a href=&quot;https://deepakness.com/tags/web-browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;web browser&lt;/a&gt;. In this post, I will be sharing my experience – the good, bad, and everything.&lt;/p&gt;
&lt;h2&gt;1. Clean and quick&lt;/h2&gt;
&lt;p&gt;When I first installed it, I didn&#39;t see any annoying bloatware at all. It looked very clean and minimal on the very first install, and it didn&#39;t even have the background image set on new tabs. Unlike the Brave browser, it &lt;strong&gt;doesn&#39;t have&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;sponsored backgrounds&lt;/li&gt;
&lt;li&gt;crypto bloats&lt;/li&gt;
&lt;li&gt;unnecessary AI push&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Below, take a look at how clean it looks.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Clean look of the Helium browser&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/helium-browser/clean-helium-browser.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;And the migration process was quicker than I thought, as I was able to import my Brave browser profiles within seconds, including history, bookmarks, and extensions.&lt;/p&gt;
&lt;h2&gt;2. Based on Chromium&lt;/h2&gt;
&lt;p&gt;To be honest, I&#39;ve always liked Chromium engine more than that of any other browser, and the Helium browser is based on Chromium project itself. It&#39;s fast and supports all Chrome browser extensions as well.&lt;/p&gt;
&lt;p&gt;In fact, it also supports the &lt;strong&gt;Manifest v2&lt;/strong&gt; extensions and comes with uBlock Origin preinstalled. And manifest v2 is a big deal for me because I want extensions like uBlock Origin and &lt;a href=&quot;https://chromewebstore.google.com/detail/scraper/mbigbapnjcgaffohmbkdlecaccepngjd&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Scraper&lt;/a&gt; to keep working correctly for me.&lt;/p&gt;
&lt;h2&gt;3. Safe and secure&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;I also noticed that it doesn&#39;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.&lt;/p&gt;
&lt;p&gt;But it still does have the &amp;quot;Use secure DNS&amp;quot; feature which I absolutely need, as I use &lt;a href=&quot;https://one.one.one.one/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare&#39;s 1.1.1.1&lt;/a&gt; DNS resolver.&lt;/p&gt;
&lt;h2&gt;4. Cool features&lt;/h2&gt;
&lt;p&gt;While I don&#39;t use extra features a lot, they&#39;re good to have, and I like them so far. For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;!bangs are awesome&lt;/li&gt;
&lt;li&gt;Split View is handy sometimes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;There&#39;s &lt;strong&gt;one minor inconvenience&lt;/strong&gt;: the browser can&#39;t play DRM-protected content, which means you won&#39;t be able to watch Netflix or Amazon Prime in the browser. And it makes sense because &lt;a href=&quot;https://github.com/imputnet/helium/issues/116&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Helium doesn&#39;t have Widevine DRM&lt;/a&gt;, as it costs tens of thousands of dollars to get. But it&#39;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.&lt;/p&gt;
&lt;p&gt;Apart from this, I&#39;m absolutely loving the browser and sticking with it.&lt;/p&gt;
&lt;p&gt;Also, it&#39;d be great if the browser was available for mobile devices as well, but I don&#39;t think that&#39;s possible as of now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Oct 8, 2025&lt;/p&gt;
&lt;p&gt;I just noticed that the Helium browser also doesn&#39;t have a translation feature, so if you&#39;re visiting a page that&#39;s not in a language you know, you can&#39;t translate it. But that&#39;s not a big deal for me, I can use some browser extension for that when needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Nov 5, 2025&lt;/p&gt;
&lt;p&gt;Now, it&#39;s been more than a month, and I&#39;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&#39;t notice any major changes – everything works as expected.&lt;/p&gt;
&lt;p&gt;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 once to translate a webpage (as Helium doesn&#39;t support translation), otherwise it&#39;s good so far.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Jan 30, 2026&lt;/p&gt;
&lt;p&gt;I loved the Helium browser as it was my default browser for almost 4 months, but I have recently &lt;a href=&quot;https://deepakness.com/blog/vivaldi-browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;migrated to the Vivaldi browser&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry><entry>
    <title>macOS App to Show Lid Angle in the Menu Bar</title>
    <link href="https://deepakness.com/blog/show-lid-angle-in-menu-bar/" />
    <updated>2025-09-11T08:51:46Z</updated>
    <id>https://deepakness.com/blog/show-lid-angle-in-menu-bar/</id>
    <summary>A macOS app that keeps showing the current angle of the MacBook screen or lid in the menu bar.</summary>
    <content type="html">&lt;p&gt;I came across &lt;a href=&quot;https://x.com/samhenrigold/status/1964428927159382261&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this cool project&lt;/a&gt; where Sam was able to access a hidden API from macOS which outputs the current angle of the lid. He built a cool app that accessed the API and showed the angle, as &lt;a href=&quot;https://x.com/samhenrigold/status/1964428927159382261&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;shown in the video here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;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&#39;s a &lt;a href=&quot;https://www.youtube.com/watch?v=0DoqINQkjO4&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;quick demo video&lt;/a&gt; showing the same.&lt;/p&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@0.3.3/src/lite-yt-embed.min.css&quot;&gt;
&lt;script defer=&quot;defer&quot; src=&quot;https://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@0.3.3/src/lite-yt-embed.min.js&quot;&gt;&lt;/script&gt;
&lt;div id=&quot;0DoqINQkjO4&quot; class=&quot;eleventy-plugin-youtube-embed&quot;&gt;&lt;lite-youtube videoid=&quot;0DoqINQkjO4&quot; style=&quot;background-image: url(&#39;https://i.ytimg.com/vi/0DoqINQkjO4/hqdefault.jpg&#39;);&quot; params=&quot;rel=0&quot;&gt;&lt;div class=&quot;lty-playbtn&quot;&gt;&lt;/div&gt;&lt;/lite-youtube&gt;&lt;/div&gt;
&lt;p&gt;I also &lt;a href=&quot;https://x.com/DeepakNesss/status/1966061409562931599&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;posted about the same on X&lt;/a&gt; along with the &lt;a href=&quot;https://github.com/deepakness/LidAngle&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;public GitHub repo&lt;/a&gt; that you can clone and run the app via Xcode on your MacBook devices.&lt;/p&gt;
&lt;p&gt;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 &lt;a href=&quot;https://www.tomshardware.com/software/developer-tools/engineer-taps-into-apples-hidden-lidanglesensor-api-to-create-a-creaky-door-simulator&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;some manual researching and reading&lt;/a&gt; about the hidden &lt;strong&gt;LidAngleSensor API&lt;/strong&gt; and how it can be accessed. And then it finally worked.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Building LidAngle app in Xcode&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/show-lid-angle-in-menu-bar/lidangle-xcode.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I still don&#39;t know how to get the final executable file from here, but I am reading about it and should also release the &lt;code&gt;.dmg&lt;/code&gt; file that one can use to directly install this with just a click.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I have added some more info about how this works and limitations &lt;a href=&quot;https://github.com/deepakness/LidAngle/blob/main/README.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;in the README.md file&lt;/a&gt; on GitHub. You can learn some interesting things about the API and the app from there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Will keep updating the post as I discover more.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Git and GitHub for Vibe Coders</title>
    <link href="https://deepakness.com/blog/git-for-vibe-coders/" />
    <updated>2026-08-06T00:00:00Z</updated>
    <published>2025-09-09T18:09:56Z</published>
    <id>https://deepakness.com/blog/git-for-vibe-coders/</id>
    <summary>Everything you need to know about Git and GitHub as a vibe coder.</summary>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;This post is &lt;strong&gt;written using AI&lt;/strong&gt; (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.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&#39;ve been coding with AI tools like Cursor, Claude Code, and OpenAI Codex for a while now, and let me tell you – it&#39;s both amazing and terrifying. One moment you&#39;re asking AI to &amp;quot;add user authentication to this app,&amp;quot; and the next moment you&#39;re staring at 200 lines of generated code wondering if it just broke your entire project.&lt;/p&gt;
&lt;p&gt;And this is called &lt;strong&gt;vibe coding&lt;/strong&gt; (&lt;a href=&quot;https://x.com/karpathy/status/1886192184808149383&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;coined by Andrej Karpathy&lt;/a&gt;) – using AI to generate code based on prompts and vibes rather than writing every line yourself. It&#39;s incredibly powerful, but it can also turn your codebase into chaos if you&#39;re not careful.&lt;/p&gt;
&lt;p&gt;That&#39;s where Git and GitHub come in. Think of them as your safety net when AI coding goes sideways.&lt;/p&gt;
&lt;h2&gt;Why vibe coders desperately need Git&lt;/h2&gt;
&lt;p&gt;Here&#39;s the thing about vibe coding: you often don&#39;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 &amp;quot;small refactor&amp;quot; that turned into a complete rewrite?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Without Git, you&#39;re basically coding without a parachute.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Git vs GitHub&lt;/h2&gt;
&lt;p&gt;I used to think Git and GitHub were the same thing. Spoiler alert: they&#39;re not.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Git&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt; is a website that hosts your Git repositories online. It&#39;s like Google Drive for your code repositories, but with extra features like Issues, Pull Requests, and Actions.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Installing Git (first things first)&lt;/h2&gt;
&lt;p&gt;Before we can configure Git, we need to make sure it&#39;s actually installed on your system. Let&#39;s check if you already have it:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see something like &lt;code&gt;git version 2.39.0&lt;/code&gt;, you&#39;re good to go! If you get a &amp;quot;command not found&amp;quot; error, you need to install Git.&lt;/p&gt;
&lt;h3&gt;On macOS&lt;/h3&gt;
&lt;p&gt;The easiest way is using Homebrew (if you have it):&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or download the installer from &lt;a href=&quot;https://git-scm.com/download/mac&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;git-scm.com&lt;/a&gt;. macOS also comes with an older version of Git that gets installed when you run &lt;code&gt;git&lt;/code&gt; for the first time, but I recommend getting the latest version.&lt;/p&gt;
&lt;h3&gt;On Windows&lt;/h3&gt;
&lt;p&gt;Download the installer from &lt;a href=&quot;https://git-scm.com/download/win&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;git-scm.com&lt;/a&gt;. It comes with Git Bash, which gives you a Unix-like terminal that&#39;s perfect for running Git commands.&lt;/p&gt;
&lt;h3&gt;On Linux&lt;/h3&gt;
&lt;p&gt;Most Linux distributions include Git in their package managers:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ubuntu/Debian:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;CentOS/RHEL/Fedora:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; yum &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# or on newer versions:&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; dnf &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once installed, run &lt;code&gt;git --version&lt;/code&gt; again to confirm it&#39;s working.&lt;/p&gt;
&lt;h2&gt;Setting up Git (the boring but necessary part)&lt;/h2&gt;
&lt;p&gt;Before you start, you need to tell Git who you are. Run these commands once:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; config &lt;span class=&quot;token parameter variable&quot;&gt;--global&lt;/span&gt; user.name &lt;span class=&quot;token string&quot;&gt;&quot;Your Name&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; config &lt;span class=&quot;token parameter variable&quot;&gt;--global&lt;/span&gt; user.email &lt;span class=&quot;token string&quot;&gt;&quot;your.email@example.com&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it. Now Git will stamp your name on every commit you make. You only need to do this once per computer.&lt;/p&gt;
&lt;h2&gt;The basic Git workflow that changed my coding life&lt;/h2&gt;
&lt;p&gt;Here&#39;s the workflow I use every single day:&lt;/p&gt;
&lt;h3&gt;1. Check what&#39;s changed&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; status&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows you which files have been modified, added, or deleted. I probably run this command 50 times a day. It&#39;s like asking Git &amp;quot;what&#39;s different since my last commit?&amp;quot;&lt;/p&gt;
&lt;h3&gt;2. Stage your changes&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells Git &amp;quot;I want to include all these changes in my next commit.&amp;quot; You can also add specific files with &lt;code&gt;git add filename.js&lt;/code&gt; if you want to be more selective.&lt;/p&gt;
&lt;h3&gt;3. Commit your changes&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; commit &lt;span class=&quot;token parameter variable&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Add user authentication system&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a permanent snapshot of your staged changes. Write a clear message describing what you did. Future you will thank you for this.&lt;/p&gt;
&lt;h3&gt;4. Push to GitHub&lt;/h3&gt;
&lt;p&gt;Before you can push, you need to connect your local repository to a GitHub repository. Here&#39;s how:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First time setup:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new repository on GitHub (go to github.com, click the &amp;quot;+&amp;quot; icon, select &amp;quot;New repository&amp;quot;)&lt;/li&gt;
&lt;li&gt;Copy the repository URL (it looks like &lt;code&gt;https://github.com/yourusername/your-repo-name.git&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Connect your local repo to GitHub:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; remote &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; origin https://github.com/yourusername/your-repo-name.git
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; branch &lt;span class=&quot;token parameter variable&quot;&gt;-M&lt;/span&gt; main
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push &lt;span class=&quot;token parameter variable&quot;&gt;-u&lt;/span&gt; origin main&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After the first setup:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; push&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first command (&lt;code&gt;git remote add origin&lt;/code&gt;) tells Git where your GitHub repository is located. The second command (&lt;code&gt;git branch -M main&lt;/code&gt;) ensures your main branch is named &amp;quot;main&amp;quot; (GitHub&#39;s default). The third command (&lt;code&gt;git push -u origin main&lt;/code&gt;) pushes your code and sets up tracking so future pushes just need &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This uploads your commits to GitHub, backing up your work and making it available to collaborators.&lt;/p&gt;
&lt;p&gt;I do this cycle constantly – sometimes every 10-15 minutes when I&#39;m actively coding with AI assistance. Each commit is a checkpoint I can return to if things go wrong.&lt;/p&gt;
&lt;h2&gt;Creating a repository (your project&#39;s home)&lt;/h2&gt;
&lt;p&gt;To start tracking a new project:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; init&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a hidden &lt;code&gt;.git&lt;/code&gt; folder in your current directory and starts tracking changes.&lt;/p&gt;
&lt;p&gt;If you want to work on an existing project from GitHub:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/username/repository-name.git&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This downloads the entire project and its history to your computer. For example, look how I am cloning a GitHub repo called &lt;a href=&quot;https://github.com/deepakness/optisharp&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;optisharp&lt;/a&gt; to my computer.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Cloning a repo from GitHub&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/git-for-vibe-coders/git-clone-command.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Branches: Your experimentation playground&lt;/h2&gt;
&lt;p&gt;Branches are where Git really shines for vibe coders. Think of them &lt;a href=&quot;https://deepakness.com/raw/git-branching/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;as parallel timelines&lt;/a&gt; where you can try things without affecting your main code.&lt;/p&gt;
&lt;p&gt;Create a new branch:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout &lt;span class=&quot;token parameter variable&quot;&gt;-b&lt;/span&gt; experiment/new-feature&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can ask AI to implement that crazy idea you had. If it works well, you can merge it back. If it breaks everything, just switch back to your main branch:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout main&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And delete the failed experiment:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; branch &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; experiment/new-feature&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I use branches for literally everything:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fix/header-bug&lt;/code&gt; for bug fixes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feature/payment-integration&lt;/code&gt; for new features&lt;/li&gt;
&lt;li&gt;&lt;code&gt;experiment/ai-suggestions&lt;/code&gt; for testing AI-generated code&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The .gitignore file (don&#39;t commit everything)&lt;/h2&gt;
&lt;p&gt;Some files shouldn&#39;t be tracked by Git. Create a &lt;code&gt;.gitignore&lt;/code&gt; file in your project root:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node_modules/
.env
*.log
dist/
.DS_Store
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This tells Git to ignore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node_modules/&lt;/code&gt; (huge folder of dependencies)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; (contains secret keys)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*.log&lt;/code&gt; (log files)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dist/&lt;/code&gt; (build output)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.DS_Store&lt;/code&gt; (macOS system files)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Never commit secrets, large binary files, or generated content. Your repository should only contain source code and configuration files.&lt;/p&gt;
&lt;h2&gt;My vibe coding safety rules&lt;/h2&gt;
&lt;p&gt;After months of AI-assisted coding, here are the habits that have saved me countless times:&lt;/p&gt;
&lt;h3&gt;Commit before asking AI to change anything major&lt;/h3&gt;
&lt;p&gt;Before telling Claude &amp;quot;refactor this entire component,&amp;quot; commit your current working state. This gives you an instant undo button.&lt;/p&gt;
&lt;h3&gt;Use descriptive commit messages&lt;/h3&gt;
&lt;p&gt;Instead of &amp;quot;updates&amp;quot; or &amp;quot;fixes,&amp;quot; write:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;Add user registration form with validation&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Fix mobile navigation menu bug&amp;quot;&lt;/li&gt;
&lt;li&gt;&amp;quot;Refactor database connection logic&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Review AI changes before committing&lt;/h3&gt;
&lt;p&gt;Use &lt;code&gt;git diff&lt;/code&gt; to see exactly what changed:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;diff&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows you line-by-line what the AI added, removed, or modified. Sometimes AI changes more than you expected.&lt;/p&gt;
&lt;h3&gt;Push frequently&lt;/h3&gt;
&lt;p&gt;Don&#39;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 &lt;code&gt;rm -rf&lt;/code&gt; in the wrong directory (yes, I&#39;ve done this).&lt;/p&gt;
&lt;h2&gt;GitHub: Your cloud backup and collaboration hub&lt;/h2&gt;
&lt;p&gt;GitHub adds powerful features on top of Git:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Issues&lt;/strong&gt;: Track bugs and feature requests
&lt;strong&gt;Pull Requests&lt;/strong&gt;: Review code changes before merging&lt;br&gt;
&lt;strong&gt;Actions&lt;/strong&gt;: Automate testing and deployment
&lt;strong&gt;Releases&lt;/strong&gt;: Tag stable versions of your code&lt;/p&gt;
&lt;p&gt;Even as a solo vibe coder, I use GitHub Issues to track what I want to build next. It&#39;s like having a todo list that&#39;s tied to your codebase.&lt;/p&gt;
&lt;h2&gt;When things go wrong (and they will)&lt;/h2&gt;
&lt;h3&gt;Undoing your last commit&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; reset &lt;span class=&quot;token parameter variable&quot;&gt;--soft&lt;/span&gt; HEAD~1&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h3&gt;Going back to a previous commit&lt;/h3&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; log &lt;span class=&quot;token parameter variable&quot;&gt;--oneline&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This shows your commit history. Find the commit you want to return to and:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; checkout abc1234&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Replace &lt;code&gt;abc1234&lt;/code&gt; with the actual commit hash)&lt;/p&gt;
&lt;h3&gt;Dealing with merge conflicts&lt;/h3&gt;
&lt;p&gt;Sometimes Git can&#39;t automatically merge changes. When this happens, Git will mark the conflicting sections in your files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
Your current code
=======
The conflicting code
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; branch-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Edit the file to keep what you want, remove the conflict markers, then commit the resolved version.&lt;/p&gt;
&lt;h2&gt;My current vibe coding setup&lt;/h2&gt;
&lt;p&gt;Here&#39;s how I actually use Git and GitHub in my day-to-day AI-assisted coding:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with a branch&lt;/strong&gt; for each new feature or experiment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commit every 15-20 minutes&lt;/strong&gt; when actively coding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Push at least once per coding session&lt;/strong&gt; for backup&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;a href=&quot;https://deepakness.com/raw/stage-commit-sync/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;clear commit messages&lt;/a&gt;&lt;/strong&gt; that explain the &amp;quot;why,&amp;quot; not just the &amp;quot;what&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review AI changes&lt;/strong&gt; with &lt;code&gt;git diff&lt;/code&gt; before committing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create Pull Requests&lt;/strong&gt; even for solo projects to review my own work&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This might seem like overkill, but it&#39;s saved me so many times when AI-generated code broke something unexpectedly.&lt;/p&gt;
&lt;h2&gt;The honest truth about learning Git&lt;/h2&gt;
&lt;p&gt;Git has a learning curve. Commands like &lt;code&gt;rebase&lt;/code&gt;, &lt;code&gt;cherry-pick&lt;/code&gt;, and &lt;code&gt;bisect&lt;/code&gt; can seem intimidating at first. But here&#39;s the thing: you don&#39;t need to master everything on day one.&lt;/p&gt;
&lt;p&gt;Start with the basics I&#39;ve covered here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;git add&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git commit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git push&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git checkout&lt;/code&gt; (for branches)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These five commands will handle 90% of your needs as a vibe coder. You can learn the advanced stuff when you actually need it.&lt;/p&gt;
&lt;p&gt;The key is to start using Git now, even if you don&#39;t understand everything. It&#39;s better to have imperfect version control than no version control at all.&lt;/p&gt;
&lt;h2&gt;Why this matters for vibe coders specifically&lt;/h2&gt;
&lt;p&gt;Traditional programmers write code line by line, understanding every character. They might go hours between commits because they&#39;re building something incrementally.&lt;/p&gt;
&lt;p&gt;Vibe coders work differently. We prompt AI, get large chunks of generated code, test it, and iterate. This creates unique challenges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Large, sudden changes&lt;/strong&gt; that might break existing functionality&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code we don&#39;t fully understand&lt;/strong&gt; initially&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rapid experimentation&lt;/strong&gt; with different approaches&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Need to roll back&lt;/strong&gt; entire features quickly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Getting started today&lt;/h2&gt;
&lt;p&gt;If you&#39;re not using Git yet, here&#39;s what to do right now:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install Git from &lt;a href=&quot;https://git-scm.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;git-scm.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Create a GitHub account at &lt;a href=&quot;https://github.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;github.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Run the config commands to set your name and email&lt;/li&gt;
&lt;li&gt;Go to your current project and run &lt;code&gt;git init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Create a &lt;code&gt;.gitignore&lt;/code&gt; file for your project type&lt;/li&gt;
&lt;li&gt;Add and commit your existing code&lt;/li&gt;
&lt;li&gt;Create a repository on GitHub and push your code&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Don&#39;t wait until you &amp;quot;understand Git better.&amp;quot; Start with the basics and learn as you go.&lt;/p&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;Git and GitHub aren&#39;t just tools for professional developers – they&#39;re essential safety equipment for anyone doing vibe coding. The ability to experiment fearlessly, knowing you can always undo changes, is liberating.&lt;/p&gt;
&lt;p&gt;I&#39;ve seen too many people lose hours of work because they didn&#39;t have version control. Don&#39;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.&lt;/p&gt;
&lt;p&gt;Your future self will thank you when you can confidently tell Claude Code or Cursor to &amp;quot;refactor this entire codebase&amp;quot; knowing you can always go back to the last working version.&lt;/p&gt;
&lt;p&gt;Happy vibe coding!&lt;/p&gt;
</content>
  </entry><entry>
    <title>How to Run Omarchy on a MacBook via UTM</title>
    <link href="https://deepakness.com/blog/omarchy-on-mac-via-utm/" />
    <updated>2025-09-02T16:20:24Z</updated>
    <id>https://deepakness.com/blog/omarchy-on-mac-via-utm/</id>
    <summary>A quick guide to running Omarchy ISO on a MacBook M2 Air for testing via UTM.</summary>
    <content type="html">&lt;p&gt;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 &lt;a href=&quot;https://omarchy.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;DHH&#39;s new Arch-based Omarchy Linux distro&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;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 &lt;a href=&quot;https://mac.getutm.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;UTM (VM for macOS)&lt;/a&gt; on my machine, and I finally got it working after almost an hour of trying different settings in UTM. And here&#39;s how it looked:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Omarchy installed on MacBook via UTM&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/omarchy-on-mac-via-utm/omarchy-live.png&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;But please note that I wasn&#39;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.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here&#39;s how I got it working...&lt;/p&gt;
&lt;h2&gt;1. Download Omarchy ISO and install UTM&lt;/h2&gt;
&lt;p&gt;The very first step is to download the Omarchy ISO from &lt;a href=&quot;https://omarchy.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;their official website&lt;/a&gt;, and save it somewhere on your computer. It was around 1.5 GB in size when I tried, but might change a bit in the future.&lt;/p&gt;
&lt;p&gt;Then you have to install UTM by downloading the &lt;code&gt;.dmg&lt;/code&gt; from &lt;a href=&quot;https://mac.getutm.app/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;their official website&lt;/a&gt;, and installing it by double-clicking on the file, just like how you install any other macOS app.&lt;/p&gt;
&lt;h2&gt;2. Set up a new virtual machine&lt;/h2&gt;
&lt;p&gt;Open the UTM app and set up a new virtual machine by clicking on the &lt;strong&gt;Create a New Virtual Machine&lt;/strong&gt; button, and below are the options you have to change – leave options, which I haven&#39;t mentioned here, as default.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Select &lt;strong&gt;Emulate&lt;/strong&gt; option, as we have to run another CPU architecture&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Linux&lt;/strong&gt; option&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;Browse&lt;/strong&gt; button and select the Omarchy ISO file, and click &lt;strong&gt;Continue&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;x86_64&lt;/strong&gt; in the Architecture dropdown, leave System as the default&lt;/li&gt;
&lt;li&gt;Make the Memory to be 8 GB i.e. 8192 MiB, and click &lt;strong&gt;Continue&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Either leave the size as 64 GiB or you can make it 32 GiB, click &lt;strong&gt;Continue&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Optional, but you can select a shared directory path, click &lt;strong&gt;Continue&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Give your VM a name and &lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Right-click on the VM in UTM sidebar, and select &lt;strong&gt;Edit&lt;/strong&gt; option&lt;/li&gt;
&lt;li&gt;Go to the Display section&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;virtio-gpu-pci&lt;/code&gt; in the Emulated Display Card dropdown (see screenshot below)&lt;/li&gt;
&lt;li&gt;Check the Retina Mode option and &lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Below I am adding some screenshots, in case that&#39;s helpful for you during the process.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Start Omarchy installation process&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/omarchy-on-mac-via-utm/utm-omarchy.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Selecting the correct Display option&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/omarchy-on-mac-via-utm/utm-display.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Let&#39;s move to the next and last step now.&lt;/p&gt;
&lt;h2&gt;3. Start the installation process&lt;/h2&gt;
&lt;p&gt;Click on the big &lt;strong&gt;Play button&lt;/strong&gt; (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 &lt;a href=&quot;https://learn.omacom.io/2/the-omarchy-manual&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Omarchy documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now it will take time... a lot of time, actually.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;After the installation, it automatically restarts and might again ask you for the details that you just provided during the installation.&lt;/p&gt;
&lt;p&gt;But &lt;strong&gt;do not&lt;/strong&gt; move forward.&lt;/p&gt;
&lt;p&gt;Instead, shut down the VM, right-click on the VM in the sidebar and select the &lt;strong&gt;Edit&lt;/strong&gt; 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).&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Clear Omarchy ISO&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/omarchy-on-mac-via-utm/clear-omarchy-iso.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now &lt;strong&gt;Save&lt;/strong&gt;, and restart the VM by clicking on the &lt;strong&gt;Play&lt;/strong&gt; button. You should be asked to enter your password and see your beautiful Omarchy desktop.&lt;/p&gt;
&lt;p&gt;And you&#39;re done!&lt;/p&gt;
&lt;p&gt;As I have mentioned in the start, this will not be very performant, but you can definitely get a glimpse of Omarchy.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Are Double Quotes Replacing Single Quotes?</title>
    <link href="https://deepakness.com/blog/double-vs-single-quotes/" />
    <updated>2025-07-20T01:16:59Z</updated>
    <id>https://deepakness.com/blog/double-vs-single-quotes/</id>
    <summary>How quotation mark usage is changing across regions, platforms, and generations.</summary>
    <content type="html">&lt;p&gt;Recently, I was editing a document and noticed that I instinctively used double quotation marks (&amp;quot; &amp;quot;) instead of single ones (&#39; &#39;). It made me pause for a second – didn&#39;t I grow up using single quotes for things like speech and dialogue?&lt;/p&gt;
&lt;p&gt;That led me down a rabbit hole of how quotation mark usage has been changing. And yeah, things have shifted a bit – but it&#39;s not as straightforward as &amp;quot;double quotes are taking over single quotes.&amp;quot;&lt;/p&gt;
&lt;h2&gt;It depends on where you&#39;re from&lt;/h2&gt;
&lt;p&gt;In the US, double quotes have always been the default. In the UK, it&#39;s traditionally been single quotes. But this line is starting to blur. Some big UK newspapers (like &lt;em&gt;The Telegraph&lt;/em&gt; and &lt;em&gt;The Times&lt;/em&gt;) now follow what&#39;s more common in the US. Even the term &amp;quot;quotation marks&amp;quot; is &lt;a href=&quot;https://stancarey.wordpress.com/2019/05/31/quotation-marks-or-inverted-commas/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;becoming more common&lt;/a&gt; than the older &amp;quot;inverted commas&amp;quot; in Britain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fun fact&lt;/strong&gt;: 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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot of a Telegraph article showing use of double quotes&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/double-vs-single-quotes/telegraph-uk.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;But look at &lt;a href=&quot;https://www.telegraph.co.uk/news/2025/07/19/influencer-who-went-viral-at-wimbledon-is-actually-ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;today&#39;s article&lt;/a&gt; in the Telegraph UK, they haven&#39;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.&lt;/p&gt;
&lt;h2&gt;But... publishing still has its own rules&lt;/h2&gt;
&lt;p&gt;Publishing houses and editors haven&#39;t all switched to one style. The &lt;a href=&quot;https://en.wikipedia.org/wiki/AP_Stylebook&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;AP Stylebook&lt;/a&gt; still &lt;a href=&quot;https://www.ragan.com/ap-style-quotation-marks/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;prefers double quotes&lt;/a&gt; in the US. Meanwhile, British publishers stick to their traditional style – unless the audience is more international. In that case, they might lean American.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Screenshot from the AP Stylebook showing quotation mark rules&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/double-vs-single-quotes/ap-style-double-quotes.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Also, with things going more digital, there&#39;s pressure to make everything consistent. For example, many digital platforms don&#39;t even treat curly quotes properly in search – which is wild if you care about accuracy in research.&lt;/p&gt;
&lt;h2&gt;Then there&#39;s texting, typing, and social media&lt;/h2&gt;
&lt;p&gt;This is where things get messy. Most of &lt;a href=&quot;https://washfm.iheart.com/featured/toby-and-chilli-mornings/content/2023-09-13-younger-generations-do-not-really-care-about-grammar/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Gen Z doesn&#39;t bother with proper punctuation&lt;/a&gt; when texting. And I totally get it – it&#39;s just faster. Pulling up smart quotes on a mobile keyboard is annoying.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Usage of quotation while texting&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/double-vs-single-quotes/quotation-in-texting.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;On social media, people have created their own ways to show quotes. Sometimes they don&#39;t even use punctuation. They just use formatting, screenshots, or emojis to make a point.&lt;/p&gt;
&lt;p&gt;So if you&#39;re typing on your phone, you&#39;re probably using straight quotes, or skipping them altogether. I do it too, especially in casual messages or tweets.&lt;/p&gt;
&lt;h2&gt;Style guides are slowly adapting&lt;/h2&gt;
&lt;p&gt;Style guides from places like Notre Dame and newer journalism groups have started updating their rules. They know people aren&#39;t typing the same way they did 10 years ago. So while they still &lt;a href=&quot;https://ndworks.nd.edu/news/editors-note-single-or-double-quotes/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;prefer double quotes in formal writing&lt;/a&gt;, they&#39;re acknowledging that habits are changing – especially online.&lt;/p&gt;
&lt;h2&gt;The real picture&lt;/h2&gt;
&lt;p&gt;If you&#39;re wondering whether single quotes are disappearing, the answer is: &lt;strong&gt;yes, somewhat&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But they&#39;re also being used differently depending on the context, and both single and double quotes are still around. It&#39;s just that we&#39;re now using them based on what feels right for the situation – and honestly, that makes sense.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Calling Myself an Internet Generalist</title>
    <link href="https://deepakness.com/blog/internet-generalist/" />
    <updated>2025-06-09T16:44:04Z</updated>
    <id>https://deepakness.com/blog/internet-generalist/</id>
    <summary>Starting to call myself an internet generalist from today.</summary>
    <content type="html">&lt;p&gt;Starting today, I&#39;m calling myself an internet generalist – because it describes me well.&lt;/p&gt;
&lt;p&gt;If you&#39;re wondering what that means:&lt;/p&gt;
&lt;p&gt;An &lt;strong&gt;internet generalist&lt;/strong&gt; is someone with a broad range of skills and knowledge related to working online. They might be familiar with things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Programming&lt;/li&gt;
&lt;li&gt;Writing and editing&lt;/li&gt;
&lt;li&gt;Online marketing&lt;/li&gt;
&lt;li&gt;Graphics design&lt;/li&gt;
&lt;li&gt;Leveraging AI, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Basically, they&#39;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.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Trying the Sarvam-M 24B Indic LLM</title>
    <link href="https://deepakness.com/blog/sarvam-m/" />
    <updated>2025-06-04T00:00:00Z</updated>
    <published>2025-05-27T03:41:59Z</published>
    <id>https://deepakness.com/blog/sarvam-m/</id>
    <summary>Sarvam AI has launched its very first hybrid Indic LLM called Sarvam-M, and I&#39;m trying to see how it performs.</summary>
    <content type="html">&lt;p&gt;Sarvam AI has released its very first &lt;a href=&quot;https://www.sarvam.ai/blogs/sarvam-m&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Hybrid Indic LLM called Sarvam-M 24b&lt;/a&gt; built on top of the Mistral-Small model (hence the &lt;code&gt;M&lt;/code&gt; in the name). It&#39;s a text-only model trained on all major Indian languages. You can run it locally on your computer &lt;a href=&quot;https://huggingface.co/sarvamai/sarvam-m&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;as it&#39;s open-source&lt;/a&gt;, use &lt;a href=&quot;https://dashboard.sarvam.ai/playground&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;on the playground&lt;/a&gt;, and also &lt;a href=&quot;https://docs.sarvam.ai/sarvam-api-docs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;available via API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;The first impression&lt;/h2&gt;
&lt;p&gt;I skimmed through &lt;a href=&quot;https://www.sarvam.ai/blogs/sarvam-m&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the launch blog post&lt;/a&gt; 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&#39;s claimed to perform:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;20% better on Indian language tasks&lt;/li&gt;
&lt;li&gt;21.6% better on math tasks&lt;/li&gt;
&lt;li&gt;17.6% better on programming tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;As mentioned in the post, they used Supervised Fine-Tuning (SFT), Reinforcement Learning with Verifiable Rewards (RLVR), and Inference Optimization to train the model.&lt;/p&gt;
&lt;h2&gt;Trying the Sarvam Playground&lt;/h2&gt;
&lt;p&gt;I &lt;a href=&quot;https://dashboard.sarvam.ai/playground&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;visited this URL&lt;/a&gt; to access the Playground and signed up using my Google account, and landed on the playground page that looked like this.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Sarvam AI Playground&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/sarvam-m/sarvam-ai-playground.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;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&#39;d like, below is the exact response that it gave.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Asking Sarvam AI to talk in Hindi&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/sarvam-m/asking-sarvam-m-to-talk-in-hindi.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;But what impressed me the most is its &amp;quot;Text to Speech&amp;quot; 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.&lt;/p&gt;
&lt;p&gt;To test further, I gave it the below paragraph in English, asked it to translate to Odia, and then converted it to &lt;a href=&quot;https://deepakness.com/audio/sarvam-m.wav&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;an audio&lt;/a&gt;. I&#39;m putting everything below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The English text:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Translated Odia text:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ସୌରଜଗତ ହେଉଛି ସୂର୍ଯ୍ୟଙ୍କ ଚାରିପାଖେ ଘୂର୍ଣ୍ଣନ କରୁଥିବା ବସ୍ତୁମାନଙ୍କର ଏକ ସମୂହ। ସୂର୍ଯ୍ୟ କେନ୍ଦ୍ରରେ ଅବସ୍ଥିତ ଏବଂ ସୌରଜଗତର ସବୁଠାରୁ ବଡ଼ ବସ୍ତୁ। ଏହା ଉତ୍ତାପ ଏବଂ ଆଲୋକ ବିସ୍ତାର କରେ ଯାହା ପୃଥିବୀରେ ଜୀବନ ସମ୍ଭବ କରିଥାଏ। ସୂର୍ଯ୍ୟଙ୍କ ଚାରିପାଖେ ପରିକ୍ରମା କରୁଥିବା 8ଟି ମୁଖ୍ୟ ଗ୍ରହ ଅଛନ୍ତି: ବୁଧ, ଶୁକ୍ର, ପୃଥିବୀ, ମଙ୍ଗଳ, ବୃହସ୍ପତି, ଶନି, ଅରୁଣ, ଏବଂ ନେପ୍ଚ୍ୟୁନ। ଏହି ଗ୍ରହମାନେ କକ୍ଷପଥ  ନାମକ ପଥରେ ଘୂର୍ଣ୍ଣନ କରନ୍ତି।
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;And the final audio:&lt;/strong&gt;&lt;/p&gt;
&lt;audio controls=&quot;&quot;&gt;
  &lt;source src=&quot;https://deepakness.com/audio/sarvam-m.wav&quot; type=&quot;audio/wav&quot;&gt;
  Your browser does not support the audio element.
&lt;/audio&gt;
&lt;p&gt;I can&#39;t read the Odia script, but I can speak it, and can say that the audio quality is very good. It speaks perfectly as they speak on the news channels.&lt;/p&gt;
&lt;p&gt;I&#39;m very impressed with the output.&lt;/p&gt;
&lt;p&gt;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 it perfectly.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Sarvam AI from Odia Speech to Text&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/sarvam-m/speech-to-text.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Similarly, other tools are good as well.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One problem though, when I put 2 paragraphs of text in the text-to-speech tool, it doesn&#39;t work and just says &amp;quot;internal server error&amp;quot;. The error should be more descriptive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Trying via the API&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token doctype&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;!&lt;/span&gt;&lt;span class=&quot;token doctype-tag&quot;&gt;DOCTYPE&lt;/span&gt; &lt;span class=&quot;token name&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Sarvam Chat&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;title&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;head&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Ask Sarvam&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;userInput&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;placeholder&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Type your question...&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token value css language-css&quot;&gt;&lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 300px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;button&lt;/span&gt; &lt;span class=&quot;token special-attr&quot;&gt;&lt;span class=&quot;token attr-name&quot;&gt;onclick&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token value javascript language-javascript&quot;&gt;&lt;span class=&quot;token function&quot;&gt;callSarvam&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Send&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;button&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Response:&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;pre&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;output&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Waiting for input...&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;pre&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token script&quot;&gt;&lt;span class=&quot;token language-javascript&quot;&gt;
    &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;callSarvam&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; userMessage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;userInput&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;https://api.sarvam.ai/v1/chat/completions&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&#39;Authorization&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Bearer YOUR_API_KEY_HERE&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token string-property property&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;system&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;you&#39;re sarvam, an llm&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;role&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;user&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; userMessage &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;sarvam-m&#39;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;output&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Error: &#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; content &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;choices&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;output&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textContent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;script&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;body&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;html&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You just need to replace &lt;code&gt;YOUR_API_KEY_HERE&lt;/code&gt; 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.&lt;/p&gt;
&lt;h3&gt;Sarvam API pricing&lt;/h3&gt;
&lt;p&gt;Below is the &lt;a href=&quot;https://dashboard.sarvam.ai/billing/pricing&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;pricing&lt;/a&gt; for different models in Indian Rupees (₹) as presented on the pricing page.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Category&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Service&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Price&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Speech APIs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Speech to Text&lt;/td&gt;
&lt;td&gt;₹30 / hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Speech to Text &amp;amp; Translate&lt;/td&gt;
&lt;td&gt;₹30 / hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Speech to Text with Diarization&lt;/td&gt;
&lt;td&gt;₹45 / hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Speech to Text, Translate &amp;amp; Diarization&lt;/td&gt;
&lt;td&gt;₹45 / hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Translate&lt;/td&gt;
&lt;td&gt;₹20 / 10K chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Transliterate&lt;/td&gt;
&lt;td&gt;₹20 / 10K chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Language Identification&lt;/td&gt;
&lt;td&gt;₹3.5 / 10K chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Text to Speech&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text to Speech&lt;/td&gt;
&lt;td&gt;₹15 / 10K chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Call Analytics&lt;/td&gt;
&lt;td&gt;₹112 / hour&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I don&#39;t know why they haven&#39;t mentioned the pricing for chat completions anywhere on the pricing page, or on any page.&lt;/p&gt;
&lt;h2&gt;But... do people care about an Indic LLM?&lt;/h2&gt;
&lt;p&gt;Yes, the question is, do people in India actually need an Indic model like this? Several people like &lt;a href=&quot;https://x.com/deedydas/status/1926089907988046261&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Deedy&lt;/a&gt; and &lt;a href=&quot;https://x.com/arpit_bhayani/status/1926104122735890910&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Arpit Bhayani&lt;/a&gt; mention that there&#39;s not enough demand for a model like this and that&#39;s why there are only a few hundred downloads of the model on Hugging Face.&lt;/p&gt;
&lt;p&gt;I think the main reasons for the low number of downloads are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;very few people care about LLMs in India as of now&lt;/li&gt;
&lt;li&gt;most people are end users and won&#39;t be downloading the model from Hugging Face, and&lt;/li&gt;
&lt;li&gt;this is not marketed well enough, people need to be made aware of what this can do&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;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&#39;s or Gemini&#39;s models without any second thoughts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🗓️ Update: June 4, 2025&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The number of downloads on &lt;a href=&quot;https://huggingface.co/sarvamai/sarvam-m&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Hugging Face&lt;/a&gt; has increased to 269,186 at the time of writing this post. But I am not seeing enough buzz about Sarvam AI on social media and also on YouTube, so it&#39;s hard to say if the number is genuine or manipulated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overall, I liked the below things:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ability to generate output in Indian languages&lt;/li&gt;
&lt;li&gt;formatting and quality of the output&lt;/li&gt;
&lt;li&gt;inference speed when asking questions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;And a few things that can be improved:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;documentation for different endpoints&lt;/li&gt;
&lt;li&gt;making people aware of the model&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also &lt;a href=&quot;https://www.youtube.com/watch?v=Fs51ydxH6XY&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;recorded a video of myself&lt;/a&gt; trying the new Sarvam-M model via the playground and API, and have put my thoughts into it.&lt;/p&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@0.3.3/src/lite-yt-embed.min.css&quot;&gt;
&lt;script defer=&quot;defer&quot; src=&quot;https://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@0.3.3/src/lite-yt-embed.min.js&quot;&gt;&lt;/script&gt;
&lt;div id=&quot;Fs51ydxH6XY&quot; class=&quot;eleventy-plugin-youtube-embed&quot;&gt;&lt;lite-youtube videoid=&quot;Fs51ydxH6XY&quot; style=&quot;background-image: url(&#39;https://i.ytimg.com/vi/Fs51ydxH6XY/hqdefault.jpg&#39;);&quot; params=&quot;rel=0&quot;&gt;&lt;div class=&quot;lty-playbtn&quot;&gt;&lt;/div&gt;&lt;/lite-youtube&gt;&lt;/div&gt;
&lt;p&gt;That&#39;s it.&lt;/p&gt;
&lt;p&gt;I am still using and testing the model and will keep updating this page if I learn something new.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Trying the Qwen3-8B Model Locally</title>
    <link href="https://deepakness.com/blog/trying-qwen3-8b/" />
    <updated>2025-05-04T02:42:52Z</updated>
    <id>https://deepakness.com/blog/trying-qwen3-8b/</id>
    <summary>Trying the qwen3-8b model locally and sharing my learnings.</summary>
    <content type="html">&lt;p&gt;I have been hearing a lot of good things about the &lt;code&gt;qwen3-8b&lt;/code&gt; model lately, in fact, I also read about it in a &lt;a href=&quot;https://simonwillison.net/2025/May/2/qwen3-8b/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;post by Simon&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So, I had to try it and I will be sharing my learnings in this post.&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;p&gt;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 &lt;a href=&quot;https://ollama.com/library/qwen3:8b&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ollama page for qwen3 here&lt;/a&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;ollama run qwen3:8b&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the &lt;code&gt;8b&lt;/code&gt; model, it downloaded a 5.2GB file.&lt;/p&gt;
&lt;h2&gt;Initial testing&lt;/h2&gt;
&lt;p&gt;At first, I just sent &amp;quot;hi&amp;quot; and only then did I find out that it&#39;s a thinking model as its thoughts were being shown between &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;/think&amp;gt;&lt;/code&gt; tags. And then it replied with &amp;quot;Hello! 😊 How can I assist you today?&amp;quot; as you can see in the below screenshot.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Running Qwen3-8B model via Ollama&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/trying-qwen3-8b/qwen3-8b-via-ollama.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Coding test&lt;/h2&gt;
&lt;p&gt;I asked it to generate a simple tic-tac-toe game by giving it the following prompt:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;create a tic tac toe game in python&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;check_winner&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; player&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Check rows&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; row &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; row&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; row&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; row&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; player&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Check columns&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; col &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; player&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Check diagonals&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; player&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; player&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;is_draw&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; row &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; row&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;print_board&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; row &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;|&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;join&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;row&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    board &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; _ &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; _ &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    current_player &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;X&#39;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        print_board&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                move &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;Player &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;current_player&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;, enter your move (1-9): &quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; move &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                    row &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;move &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
                    col &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;move &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;row&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                        board&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;row&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; current_player
                        &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                        &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Cell already occupied. Try again.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Invalid input. Please enter a number between 1 and 9.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;except&lt;/span&gt; ValueError&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Invalid input. Please enter a number between 1 and 9.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; check_winner&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; current_player&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            print_board&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;Player &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;current_player&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; wins!&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; is_draw&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            print_board&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;It&#39;s a draw!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;
        current_player &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;O&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; current_player &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;X&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;X&#39;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; __name__ &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;__main__&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    main&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It looks like this when you play the game - it perfectly handles the win, lose, and draw cases.&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;| | | |
| | | |
| | | |
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!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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&#39;s due to my computer&#39;s power, and it would have been faster on a computer with higher configuration.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you want to take a look at the entire output, including the &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; tags, you can click here to &lt;a href=&quot;https://deepakness.com/docs/trying-qwen3-8b.txt&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;see this .txt file&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Apart from this, I asked multiple simple coding and tech related questions and I&#39;d say that I am very satisfied with it. It performs surprisingly well for its size.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Learning about sqlite3 WASM</title>
    <link href="https://deepakness.com/blog/sqlite3-wasm/" />
    <updated>2025-04-24T01:22:40Z</updated>
    <id>https://deepakness.com/blog/sqlite3-wasm/</id>
    <summary>Recently discovered that sqlite3 WebAssembly can run directly in the browser, so learning more about it.</summary>
    <content type="html">&lt;p&gt;I love sqlite3 and have used it for a few simple projects, but I recently came to know that &lt;a href=&quot;https://www.notion.com/blog/how-we-sped-up-notion-in-the-browser-with-wasm-sqlite&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Notion uses sqlite3 WASM&lt;/a&gt; in the browser for client-side caching – they have been able to improve their page navigation times by 20% by doing so.&lt;/p&gt;
&lt;p&gt;I found it really interesting, so I started reading more about it. Basically, this post is just my notes on the topic – I will be noting down whatever I read.&lt;/p&gt;
&lt;h2&gt;What is sqlite3 WASM?&lt;/h2&gt;
&lt;p&gt;sqlite3 WASM or WebAssembly is a version of the SQLite database that&#39;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.&lt;/p&gt;
&lt;p&gt;And this version is &lt;a href=&quot;https://sqlite.org/wasm/doc/tip/about.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;officially supported&lt;/a&gt; by the SQLite team.&lt;/p&gt;
&lt;h2&gt;Features&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Runs the complete SQLite engine within the browser using WebAssembly while supporting standard SQL queries and database operations without server-side components.&lt;/li&gt;
&lt;li&gt;Supports multiple JavaScript APIs, like:
&lt;ul&gt;
&lt;li&gt;Low-level C API bindings that provide direct access to SQLite&#39;s C API&lt;/li&gt;
&lt;li&gt;High-level object-oriented API that offers a more JavaScript-friendly interface&lt;/li&gt;
&lt;li&gt;Worker-based API that enables operations in web-workers to prevent blocking the main thread&lt;/li&gt;
&lt;li&gt;Promise-based API that simplifies asynchronous operations with a Promise-based interface&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Utilizes the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;OPFS (Origin Private File System)&lt;/a&gt; for persistent storage, so that the data remains available across sessions.&lt;/li&gt;
&lt;li&gt;Allows the creation of virtual tables and table-valued functions directly in JavaScript and also supports custom builds with additional features or optimizations.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;How Notion is using it&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Notion uses OPFS to store data locally - persistent across sessions&lt;/li&gt;
&lt;li&gt;To avoid slowing down, they run database operations in Web Workers in the background&lt;/li&gt;
&lt;li&gt;Only the active tab handles all database writes, and other tabs send their request to this active tab&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&#39;s how they explain this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[...] only one tab is permitted to actually use its Web Worker. A SharedWorker is responsible for managing which is the &amp;quot;active tab.&amp;quot; When the active tab closes, the SharedWorker knows to select a new active tab.&lt;/p&gt;
&lt;p&gt;To execute any SQLite query, the main thread of each tab sends that query to the SharedWorker, which redirects to the active tab&#39;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.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I also found a &lt;a href=&quot;https://github.com/rhashimoto/wa-sqlite/discussions/81&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;great discussion on the topic&lt;/a&gt; on GitHub that you must check out.&lt;/p&gt;
&lt;p&gt;I recommend reading the &lt;a href=&quot;https://www.notion.com/blog/how-we-sped-up-notion-in-the-browser-with-wasm-sqlite&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;blog post by Notion&lt;/a&gt; as they have explained all the issues they were encountering and how they fixed it.&lt;/p&gt;
&lt;h2&gt;Who uses sqlite3 WASM&lt;/h2&gt;
&lt;p&gt;Some notable examples would be:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Notion: as we already discussed in the post&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://sqlime.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SQLime&lt;/a&gt;: an online SQLite playground that runs entirely in the browser&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.evolu.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Evolu.dev&lt;/a&gt;: a local-first platform designed for developers&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Resources to learn more&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sqlite.org/wasm/doc/trunk/index.md&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;sqlite3 WebAssembly &amp;amp; JavaScript Documentation Index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.chrome.com/blog/sqlite-wasm-in-the-browser-backed-by-the-origin-private-file-system&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;SQLite Wasm in the browser backed by the OPFS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=33374402&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;A discussion about the same on HackerNews&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&#39;s it.&lt;/p&gt;
&lt;p&gt;I will be reading more about it, and will keep this post updated.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Trying the LibreWolf Browser</title>
    <link href="https://deepakness.com/blog/trying-librewolf-browser/" />
    <updated>2025-04-21T00:00:00Z</updated>
    <published>2025-04-18T02:03:55Z</published>
    <id>https://deepakness.com/blog/trying-librewolf-browser/</id>
    <summary>Trying the LibreWolf web browser and will see if it becomes my default browser.</summary>
    <content type="html">&lt;p&gt;While browsing the &lt;a href=&quot;https://www.reddit.com/r/browsers/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;r/browsers on Reddit&lt;/a&gt;, 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 &lt;a href=&quot;https://deepakness.com/blog/trying-ungoogled-chromium/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;for certain reasons&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;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&#39;s start from the start.&lt;/p&gt;
&lt;h2&gt;1. Installation&lt;/h2&gt;
&lt;p&gt;I am using a MacBook Air M2 and the installation process seemed straightforward initially. I could install it through Homebrew by running the following command, or download the &lt;code&gt;.dmg&lt;/code&gt; file and then install.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--cask&lt;/span&gt; librewolf&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I used the Homebrew method and it got installed after a minute. But when I tried opening the app, it didn&#39;t open and showed the following error dialog. For your information, currently, I&#39;m on the latest macOS Sequoia 15.4.1.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Error while installing LibreWolf browser on macOS&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/trying-librewolf-browser/librewolf-error.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I started looking and found this Reddit thread, tried a bunch of things, and finally found a solution. I have briefly &lt;a href=&quot;https://deepakness.com/raw/librewolf-browser-on-mac&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;explained about the same in this post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, I got it to open.&lt;/p&gt;
&lt;h2&gt;2. Things I liked&lt;/h2&gt;
&lt;p&gt;I liked how clean it looked, but I went one step further and disabled &lt;strong&gt;shortcuts&lt;/strong&gt; and &lt;strong&gt;recent activity&lt;/strong&gt; from showing on the homepage and then it looked even cleaner.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The LibreWolf web browser&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-librewolf-browser/librewolf-browser-1.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, let&#39;s go through the things I liked.&lt;/p&gt;
&lt;h3&gt;Comes with uBlock Origin preinstalled&lt;/h3&gt;
&lt;p&gt;Yes, I didn&#39;t even have to install an adblock extension as it already came with &lt;a href=&quot;https://github.com/gorhill/uBlock#ublock-origin&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the uBlock Origin extension&lt;/a&gt; installed. The extension was also already pinned to the toolbar, as you can see in the top-right of the above screenshot.&lt;/p&gt;
&lt;p&gt;In fact, after Google Chrome started enforcing the Manifest v3 for extensions, most of the Chromium-based browsers don&#39;t support the uBlock Origin out of the box now. And as LibreWolf is Firefox based, that&#39;s not an issue here with the extension.&lt;/p&gt;
&lt;h3&gt;Supports Firefox sync&lt;/h3&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;LibreWolf sync through Firefox Sync&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-librewolf-browser/librewolf-sync.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can also copy-paste this &lt;code&gt;about:preferences#sync&lt;/code&gt; in your address bar and it will take you to the sync page. But obviously, it&#39;s optional.&lt;/p&gt;
&lt;h3&gt;Vertical tabs&lt;/h3&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Horizontal and Vertical Tabs feature&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-librewolf-browser/horizontal-vertical-tabs.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;For this, you&#39;d go in the Browser Layout section on the &lt;code&gt;about:preferences#general&lt;/code&gt; page, as shown above. From here, the sidebar can also be enabled.&lt;/p&gt;
&lt;h3&gt;Forces web content to use light theme&lt;/h3&gt;
&lt;p&gt;By default, the &lt;code&gt;ResistFingerprinting&lt;/code&gt; feature is enabled so the browser doesn&#39;t let websites see your device settings (whether it&#39;s on dark/light mode) and forces websites to show their content in the light mode only.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Website appearance&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-librewolf-browser/website-appearance.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I prefer light mode, so it&#39;s not a problem for me. But if you don&#39;t like it, you will have to disable the feature and that will make the browser a bit less secure.&lt;/p&gt;
&lt;h3&gt;Hardened security settings&lt;/h3&gt;
&lt;p&gt;I really liked how the security on the browser is super-hardened out of the box.&lt;/p&gt;
&lt;p&gt;For example, the &amp;quot;Delete cookies and site data when LibreWolf is closed&amp;quot; feature is turned on by default, which means all cookies will be cleared when you close the browser and you will be logged out from all websites. I don&#39;t need this, so I have disabled it.&lt;/p&gt;
&lt;p&gt;Similarly, there are other options also that I liked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple DNS over HTTPS providers&lt;/li&gt;
&lt;li&gt;Ask to save passwords, addresses, and payment methods is turned off by default&lt;/li&gt;
&lt;li&gt;HTTPS-only mode is enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Multiple profiles option&lt;/h3&gt;
&lt;p&gt;Though the profiles option needs to be turned on manually, it&#39;s a great feature. I had to go to &lt;code&gt;about:config&lt;/code&gt;, search for &lt;code&gt;browser.profiles.enabled&lt;/code&gt;, and set it to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Multiple profiles in LibreWolf browser&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-librewolf-browser/librewolf-profiles.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;I created two profiles – personal and work – and gave each a different color so that they are easily identifiable. I also really liked the fact that both profiles open as separate apps. I added them both to the Dock, so I can open any profile with just one click.&lt;/p&gt;
&lt;p&gt;I found &lt;a href=&quot;https://www.reddit.com/r/LibreWolf/comments/1fep173/easy_way_to_switch_between_profiles/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this Reddit discussion&lt;/a&gt; to be very helpful when doing all this.&lt;/p&gt;
&lt;h2&gt;3. Things I didn&#39;t like&lt;/h2&gt;
&lt;p&gt;Apart from the things I liked, I also didn&#39;t like the fact that &amp;quot;profiles&amp;quot; option is not turned on by default.&lt;/p&gt;
&lt;p&gt;That&#39;s it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To be honest, security-wise, I liked LibreWolf the most out of all the browsers I have used as of now.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Sometimes, it becomes very confusing.&lt;/p&gt;
&lt;p&gt;For now, I am sticking with the &lt;a href=&quot;https://deepakness.com/blog/helium-browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Brave browser&lt;/a&gt;, until I find something else.&lt;/p&gt;
</content>
  </entry><entry>
    <title>Trying the Ungoogled Chromium Browser</title>
    <link href="https://deepakness.com/blog/trying-ungoogled-chromium/" />
    <updated>2025-04-18T00:00:00Z</updated>
    <published>2025-04-10T14:16:02Z</published>
    <id>https://deepakness.com/blog/trying-ungoogled-chromium/</id>
    <summary>Was hearing a lot of good things about the Ungoogled Chromium browser, so gave it a go.</summary>
    <content type="html">&lt;p&gt;So... I tried switching to the &lt;a href=&quot;https://github.com/ungoogled-software/ungoogled-chromium&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Ungoogled Chromium browser&lt;/a&gt; as my main browser, instead of the Brave browser that I had been using before. To be honest, I sometimes find Brave&#39;s push for crypto and AI a bit annoying, and that&#39;s what made me look for an alternative. I&#39;ve tried a few browsers before, so this time I thought I&#39;d give Ungoogled Chromium a shot.&lt;/p&gt;
&lt;p&gt;In this blog post, I will explain the installation process, first impressions, what I liked, what I disliked, and whether I will be using this as my main browser or not.&lt;/p&gt;
&lt;h2&gt;The installation process&lt;/h2&gt;
&lt;p&gt;I am, currently, using the MacBook Air M2 as my main computer and the installation process was very easy. I installed the &lt;a href=&quot;https://formulae.brew.sh/cask/ungoogled-chromium&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;ungoogled-chromium&lt;/a&gt; version via Homebrew by running the following command in the terminal, which took around 30 seconds to complete.&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;brew &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--cask&lt;/span&gt; ungoogled-chromium&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I opened the browser, and was greeted by this &lt;code&gt;ungoogled-first-run&lt;/code&gt; screen that you can see in the screenshot below. It looked very clean at first and I wasn&#39;t asked to log-in in order to sync my settings as Google Chrome normally does.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Ungoogled browser first run&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-1.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, it was time to test it properly.&lt;/p&gt;
&lt;h2&gt;Testing as per my use-cases&lt;/h2&gt;
&lt;h3&gt;1. Changing the default search engine&lt;/h3&gt;
&lt;p&gt;At first, there was no default search engine set so I opened settings and selected DuckDuckGo (which was pre-listed 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 &lt;code&gt;Manage search engines and site search&lt;/code&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Changing the default search engine&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-2.jpeg&quot;&gt;&lt;/p&gt;
&lt;h3&gt;2. Installing Chrome extensions&lt;/h3&gt;
&lt;p&gt;I visited the Chrome Web Store and opened random extensions, but wasn&#39;t able to install as it was showing an error of &amp;quot;Item currently unavailable&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Can&#39;t install Chrome extensions&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-3.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;I directly visited the &lt;a href=&quot;https://github.com/gorhill/uBlock/releases&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;uBlock Origin&#39;s GitHub repo&lt;/a&gt; and downloaded the &lt;code&gt;.zip&lt;/code&gt; file for the Chromium build. The extension got installed with no errors and was working perfectly as well.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Way to install any Chrome extension&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-4.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;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 &lt;code&gt;chrome://flags&lt;/code&gt; and install a Chrome extension called &lt;a href=&quot;https://github.com/NeverDecaf/chromium-web-store&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;chromium-web-store&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Enabling all Chrome extensions to install&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-5.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;After following the steps, the &amp;quot;Add to Chrome&amp;quot; button started showing on all extensions from the Chrome Web Store, as you can see in the screenshot above. By the way, &lt;a href=&quot;https://deepakness.com/blog/creating-first-chrome-extension/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;X Mute is a Chrome extension&lt;/a&gt; that I have created – it can mute multiple words/phrases on X (Twitter) at once, and not to mention, it&#39;s free to use.&lt;/p&gt;
&lt;h3&gt;3. Profiles and developer tools&lt;/h3&gt;
&lt;p&gt;Somewhere I read that the Ungoogled Chromium browser doesn&#39;t support creating multiple profiles, but that&#39;s not true now. I successfully created two profiles named &lt;code&gt;personal&lt;/code&gt; and &lt;code&gt;work&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Developer tools&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-7.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;I also tried opening the developer tools and it worked as expected – just like how it does in the Google Chrome browser.&lt;/p&gt;
&lt;h3&gt;4. Gmail and YouTube&lt;/h3&gt;
&lt;p&gt;I also logged into Gmail and it does work, and it stays logged in even after I quit and re-open the browser. YouTube works perfectly as well, no lags, or no missing features - theatre and fullscreen modes worked as well.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Gmail and YouTube&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-8.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;I also logged into X (Twitter), and it also worked as expected.&lt;/p&gt;
&lt;h3&gt;5. Secure DNS and Google&lt;/h3&gt;
&lt;p&gt;I didn&#39;t expect that the &amp;quot;Use secure DNS&amp;quot; feature would be there in the browser, but it was there and worked as expected. I normally use &lt;a href=&quot;https://one.one.one.one/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Cloudflare&#39;s 1.1.1.1 DNS resolver&lt;/a&gt; which reduces the latency a bit and also makes everything neutral. I was able to turn it on and use it.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Secure DNS features&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-9.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;Also, I noticed a funny thing that it says &amp;quot;You and Google&amp;quot; but Google Sync doesn&#39;t work as it&#39;s disabled (which does make sense).&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Secure DNS features&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-10.jpeg&quot;&gt;&lt;/p&gt;
&lt;h3&gt;6. DRM issues&lt;/h3&gt;
&lt;p&gt;I tried opening a few Udemy courses to test the DRM features and it didn&#39;t work. Some courses played without any issues, but DRM protected courses didn&#39;t play, even after trying multiple times.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Secure DNS features&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/ungoogled-chromium-12.jpeg&quot;&gt;&lt;/p&gt;
&lt;p&gt;The workaround could have been to follow &lt;a href=&quot;https://ungoogled-software.github.io/ungoogled-chromium-wiki/faq#how-do-i-install-widevine-cdm&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;this tutorial on their FAQ page&lt;/a&gt;, but I didn&#39;t have that much patience. It&#39;s something like, you&#39;ll have to download the Google Chrome official installer file, mount it, and run a terminal command to install the Widevine CDM.&lt;/p&gt;
&lt;h3&gt;7. Benchmark testing&lt;/h3&gt;
&lt;p&gt;I used the &lt;a href=&quot;https://browserbench.org/Speedometer3.1/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;BrowserBench Speedometer to run tests&lt;/a&gt; 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.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;BrowserBench Speedometer&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/trying-ungoogled-chromium/browserbench-speedometer.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;The left is Google Chrome stat (33.0) and the right is the Ungoogled Chromium stat (31.2). But I don&#39;t think it&#39;s a deal-breaker, as the difference isn&#39;t much.&lt;/p&gt;
&lt;h3&gt;8. Experienced a bug&lt;/h3&gt;
&lt;p&gt;Lastly, I experienced a weird bug as well. The uBlock Origin extension that I installed manually, automatically got uninstalled when I quit the browser using &lt;code&gt;cmd + q&lt;/code&gt; and re-opened it. I installed the extension again and then it again got uninstalled.&lt;/p&gt;
&lt;p&gt;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&#39;s see if I change my mind.&lt;/p&gt;
&lt;p&gt;For now, I will keep using the &lt;a href=&quot;https://deepakness.com/blog/helium-browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;Brave browser&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Apr 18, 2025&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;After this, I will be &lt;a href=&quot;https://deepakness.com/blog/trying-librewolf-browser/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;trying the LibreWolf browser&lt;/a&gt; and let&#39;s see if that meets my expectations.&lt;/p&gt;
</content>
  </entry><entry>
    <title>MP3Detective: AI-Powered Music Metadata Manager</title>
    <link href="https://deepakness.com/blog/mp3detective/" />
    <updated>2025-04-06T02:31:03Z</updated>
    <id>https://deepakness.com/blog/mp3detective/</id>
    <summary>Learn how I built MP3Detective - a Python tool that uses AI to automatically fix MP3 metadata. Works with any language, tested with 95% accuracy.</summary>
    <content type="html">&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;So, I decided to build my own tool using OpenAI&#39;s GPT-4o model. And... say hello to &lt;a href=&quot;https://github.com/deepakness/mp3detective&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;MP3Detective&lt;/a&gt; – a Python tool that automatically finds and updates MP3 metadata.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;MP3Detective Demo&quot; decoding=&quot;async&quot; loading=&quot;eager&quot; fetchpriority=&quot;high&quot; src=&quot;https://assets.deepakness.com/blog/mp3detective/mp3detective-demo.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;The tool is quite simple. You just:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Put your MP3 files in the &lt;code&gt;input&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;Run the Python script&lt;/li&gt;
&lt;li&gt;Get your updated files in the &lt;code&gt;output&lt;/code&gt; folder&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;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&#39;s expected.&lt;/p&gt;
&lt;p&gt;I also have a &lt;a href=&quot;https://www.youtube.com/watch?v=sS35tyAsnYE&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;quick video&lt;/a&gt; showing how it works:&lt;/p&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@0.3.3/src/lite-yt-embed.min.css&quot;&gt;
&lt;script defer=&quot;defer&quot; src=&quot;https://cdn.jsdelivr.net/gh/paulirish/lite-youtube-embed@0.3.3/src/lite-yt-embed.min.js&quot;&gt;&lt;/script&gt;
&lt;div id=&quot;sS35tyAsnYE&quot; class=&quot;eleventy-plugin-youtube-embed&quot;&gt;&lt;lite-youtube videoid=&quot;sS35tyAsnYE&quot; style=&quot;background-image: url(&#39;https://i.ytimg.com/vi/sS35tyAsnYE/hqdefault.jpg&#39;);&quot; params=&quot;rel=0&quot;&gt;&lt;div class=&quot;lty-playbtn&quot;&gt;&lt;/div&gt;&lt;/lite-youtube&gt;&lt;/div&gt;
&lt;h2&gt;Building the tool&lt;/h2&gt;
&lt;p&gt;I used Python to build this tool because it has great libraries for handling MP3 files. The main pieces are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI&#39;s GPT-4o for identifying song details&lt;/li&gt;
&lt;li&gt;&lt;code&gt;eyeD3&lt;/code&gt; for reading/writing MP3 tags&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tqdm&lt;/code&gt; for showing nice progress bars&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&#39;s the folder structure of how everything is placed:&lt;/p&gt;
&lt;pre class=&quot;language-plaintext&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-plaintext&quot;&gt;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&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The tool creates a copy of your original files, so you don&#39;t lose anything. It also keeps a detailed log of what it&#39;s doing, which helps if something goes wrong.&lt;/p&gt;
&lt;h2&gt;Real-world testing&lt;/h2&gt;
&lt;p&gt;I tried it with about 1,500 old Hindi songs first. Here&#39;s what I found:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For most songs, it found the correct details&lt;/li&gt;
&lt;li&gt;Some very old songs (1950s-60s) were tricky&lt;/li&gt;
&lt;li&gt;Sometimes it couldn&#39;t decide between two similar song versions&lt;/li&gt;
&lt;li&gt;The original files stayed safe in the input folder&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And this is what it shows in the terminal after the process finishes.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Terminal output after the task finishes&quot; decoding=&quot;async&quot; loading=&quot;lazy&quot; src=&quot;https://assets.deepakness.com/blog/mp3detective/terminal-output.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Using it yourself&lt;/h2&gt;
&lt;p&gt;Want to try it? It&#39;s pretty simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get the code &lt;a href=&quot;https://github.com/deepakness/mp3detective&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;from GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Install Python if you don&#39;t have it&lt;/li&gt;
&lt;li&gt;Add your OpenAI API key&lt;/li&gt;
&lt;li&gt;Put your MP3 files in the input folder&lt;/li&gt;
&lt;li&gt;Run the script&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&#39;s it! The tool will do its magic and you&#39;ll find your updated files in the output folder.&lt;/p&gt;
&lt;p&gt;The code is open source, so feel free to try it out or even make it better!&lt;/p&gt;
</content>
  </entry>
</feed>
