<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Michifumi&apos;s Blog</title><description>Dispatches from Michifumi&apos;s command chair.</description><link>https://michifumi.de/</link><language>en-us</language><image><url>https://michifumi.de/icon-rss.png</url><title>Michifumi&apos;s Blog</title><link>https://michifumi.de/</link></image><item><title>Bypass E5 OneDrive 10GB Limit: Mount SharePoint</title><link>https://michifumi.de/blog/2026-09-21-bypass-e5-onedrive-10gb-limit-mount-sharepoint/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-09-21-bypass-e5-onedrive-10gb-limit-mount-sharepoint/</guid><description>Bypass Microsoft 365 Developer E5 10GB OneDrive limits and macOS sync bugs using a dual-mount SharePoint setup with Rclone and FUSE-T.</description><pubDate>Mon, 21 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Bypass Microsoft 365 Developer E5 10GB OneDrive limits and macOS sync bugs using a dual-mount SharePoint setup with Rclone and FUSE-T.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-09-21/sharepoint-storage.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;a href=&quot;https://admin.cloud.microsoft/?#/reportsUsage/SharePointStorage&quot;&gt;SharePoint Storage&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This guide resolves the 10GB personal OneDrive quota limitation on Microsoft 365 Developer E5 subscriptions by leveraging the 1.24TB tenant-wide SharePoint storage pool with a &lt;strong&gt;Dual-Mount Architecture&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;~/SharePoint&lt;/code&gt; (Raw Mount)&lt;/strong&gt;: Dedicated to large video files and general media. Files remain unencrypted in the cloud so you can stream or download them anywhere (SharePoint web portal, OneDrive mobile app, Infuse or VLC on Apple TV) without requiring Rclone or decryption keys.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;~/SharePointVault&lt;/code&gt; (Encrypted Overlay Mount)&lt;/strong&gt;: Dedicated to sensitive records, personal documents, and private backups. Uses client-side zero-knowledge encryption via &lt;strong&gt;Rclone Crypt&lt;/strong&gt; to protect data from cloud inspection and eliminate SharePoint metadata alteration loops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It completely bypasses native macOS OneDrive client issues, such as &lt;code&gt;fileproviderd&lt;/code&gt; circular deadlocks and 0% progress freezes, delivering native virtual drives with automated space reclamation (on-demand caching) and transparent on-the-fly decryption.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;0. Create a Communication Site in SharePoint&lt;/h2&gt;
&lt;p&gt;Before installing tools and configuring Rclone, you must create a dedicated &lt;strong&gt;Communication site&lt;/strong&gt; in your Microsoft 365 tenant to host your files.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-09-21/communication-site-sharepoint.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;Why a Communication Site is Essential&lt;/h3&gt;
&lt;p&gt;Microsoft SharePoint treats Team sites and Communication sites fundamentally differently when it comes to document versioning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Team Sites (Do Not Use)&lt;/strong&gt;: Team sites are tied to Microsoft 365 Groups and Microsoft Teams. Microsoft mandates document version history on Team sites, enforcing a strict minimum of 100 to 500 major versions that &lt;strong&gt;cannot be disabled&lt;/strong&gt;. If you store large files (such as 20GB–50GB video files, disk images, or encrypted vault chunks) in a Team site, any minor file modification or re-upload causes SharePoint to duplicate the entire multi-gigabyte payload into version history, silently exhausting your 1.24TB tenant storage pool within days.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Communication Sites (Essential)&lt;/strong&gt;: Only standalone Communication sites allow administrators to set Document Version History to &lt;strong&gt;“No versioning”&lt;/strong&gt; in the classic library settings. Disabling versioning ensures that overwriting or modifying a file consumes exactly the size of the current file with zero hidden storage bloat.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Creating the Site&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Sign in to your Microsoft 365 portal and navigate to SharePoint (&lt;code&gt;https://&amp;lt;tenant&amp;gt;.sharepoint.com/_layouts/15/sharepoint.aspx/build&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Site&lt;/strong&gt; in the top navigation bar.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Communication site&lt;/strong&gt; (do &lt;em&gt;not&lt;/em&gt; choose Team site).&lt;/li&gt;
&lt;li&gt;Choose the &lt;strong&gt;Blank&lt;/strong&gt; template, enter a site name (such as &lt;code&gt;Storage&lt;/code&gt; or &lt;code&gt;Drive&lt;/code&gt;), and finish the creation wizard.&lt;/li&gt;
&lt;li&gt;In &lt;a href=&quot;#2-configure-rclone-with-sharepoint-and-crypt&quot;&gt;Section 2&lt;/a&gt;, select this newly created Communication site when Rclone prompts you to bind your SharePoint remote.&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;Once your Communication site is created, make sure to disable version history on its default document library before uploading large files. Follow the step-by-step instructions in &lt;a href=&quot;#preventing-sharepoint-version-bloat-critical&quot;&gt;Preventing SharePoint Version Bloat (Critical)&lt;/a&gt; in Section 5.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Prerequisites and Installation&lt;/h2&gt;
&lt;p&gt;Mounting a cloud drive as a local filesystem on macOS requires &lt;strong&gt;MacPorts&lt;/strong&gt; (to install Rclone with mount capabilities) and &lt;strong&gt;FUSE-T&lt;/strong&gt; (which provides a user-space FUSE implementation without requiring macOS kernel extensions or lowering system security settings).&lt;/p&gt;
&lt;h3&gt;Step 1: Install MacPorts&lt;/h3&gt;
&lt;p&gt;If you do not already have MacPorts installed on your Mac:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install the Xcode Command Line Tools:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;xcode-select&lt;/span&gt;&lt;span&gt; --install&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download and run the official package installer matching your macOS version from the &lt;a href=&quot;https://www.macports.org/install.php&quot;&gt;MacPorts Installation Guide&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open a new Terminal window and verify that the &lt;code&gt;port&lt;/code&gt; command is available:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;port&lt;/span&gt;&lt;span&gt; version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 2: Install FUSE-T&lt;/h3&gt;
&lt;p&gt;Mounting a cloud drive as a local filesystem via Rclone on macOS requires a FUSE framework. FUSE-T is recommended because it runs purely in user space and eliminates the need to reboot into Recovery Mode or lower system security settings:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Visit the &lt;a href=&quot;https://github.com/macos-fuse-t/fuse-t/releases&quot;&gt;FUSE-T GitHub Releases&lt;/a&gt; page.&lt;/li&gt;
&lt;li&gt;Download and run the latest &lt;code&gt;fuse-t-macos-installer-x.x.x.pkg&lt;/code&gt; installer.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 3: Install Rclone with Mount Support via MacPorts&lt;/h3&gt;
&lt;p&gt;The default &lt;code&gt;rclone&lt;/code&gt; port in MacPorts does not include mount capabilities. Install it with the &lt;code&gt;+mount&lt;/code&gt; variant:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; port&lt;/span&gt;&lt;span&gt; selfupdate&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; port&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; rclone&lt;/span&gt;&lt;span&gt; +mount&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;WARNING&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do Not Install Rclone via Homebrew for Mounting&lt;/strong&gt;:
Homebrew’s &lt;code&gt;rclone&lt;/code&gt; formula explicitly disables the FUSE &lt;code&gt;mount&lt;/code&gt; subcommand on macOS. Running &lt;code&gt;rclone mount&lt;/code&gt; with a Homebrew build terminates immediately with &lt;code&gt;CRITICAL: Fatal error: failed to mount FUSE fs: rclone mount is not supported on MacOS when rclone is installed via Homebrew&lt;/code&gt;. Always use MacPorts (&lt;code&gt;rclone +mount&lt;/code&gt;) or the official standalone binary from &lt;a href=&quot;https://rclone.org/downloads/&quot;&gt;rclone.org&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Step 4: Bridge FUSE-T to Rclone (&lt;code&gt;libfuse.2.dylib&lt;/code&gt; Symlink)&lt;/h3&gt;
&lt;p&gt;Rclone’s FUSE integration layer on macOS (&lt;code&gt;cgofuse&lt;/code&gt;) searches for several candidate library filenames at runtime (&lt;code&gt;libfuse.2.dylib&lt;/code&gt;, &lt;code&gt;libosxfuse.2.dylib&lt;/code&gt;, and &lt;code&gt;libfuse-t.dylib&lt;/code&gt;). While modern Rclone builds include &lt;code&gt;libfuse-t.dylib&lt;/code&gt; in their search candidates, creating this symlink ensures universal compatibility across all FUSE utilities and eliminates potential runtime lookup delays:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; ln&lt;/span&gt;&lt;span&gt; -sf&lt;/span&gt;&lt;span&gt; /usr/local/lib/libfuse-t.dylib&lt;/span&gt;&lt;span&gt; /usr/local/lib/libfuse.2.dylib&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignore MacPorts’ &lt;code&gt;macfuse.fs&lt;/code&gt; Post-Install Recommendation&lt;/strong&gt;:
When compiling &lt;code&gt;rclone +mount&lt;/code&gt;, MacPorts pulls its internal &lt;code&gt;macfuse&lt;/code&gt; port as a build dependency to satisfy C compilation headers. Upon completion, MacPorts displays a note suggesting:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;sudo ln -fsn /opt/local/Library/Filesystems/macfuse.fs /Library/Filesystems/macfuse.fs&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do not run this command&lt;/strong&gt;. Linking &lt;code&gt;macfuse.fs&lt;/code&gt; attempts to load the legacy macFUSE kernel extension (kext), which triggers macOS security alerts on Apple Silicon Macs requiring you to boot into Recovery Mode and lower security to “Reduced Security”. Linking &lt;code&gt;libfuse-t.dylib&lt;/code&gt; to &lt;code&gt;libfuse.2.dylib&lt;/code&gt; ensures Rclone routes all calls purely through &lt;strong&gt;FUSE-T&lt;/strong&gt; in user space (via FSKit or NFS), leaving your system on &lt;strong&gt;Full Security&lt;/strong&gt; with zero kernel extensions or reboots required.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Configure Rclone with SharePoint and Crypt&lt;/h2&gt;
&lt;p&gt;To ensure total data privacy, prevent SharePoint from corrupting Office files with injected metadata, and eliminate filename character restrictions, we configure a base SharePoint remote and overlay it with Rclone’s native encryption layer (&lt;code&gt;crypt&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;Step 1: Authorize the SharePoint Base Remote (&lt;code&gt;sp&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;Authorize and bind the dedicated SharePoint site using Rclone’s built-in configuration wizard:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start the wizard&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;rclone&lt;/span&gt;&lt;span&gt; config&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Configuration Prompts&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enter &lt;code&gt;n&lt;/code&gt; to create a new remote, and name it &lt;code&gt;sp&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Storage&amp;gt;&lt;/code&gt;: Find &lt;code&gt;Microsoft OneDrive&lt;/code&gt; and choose it (handles both OneDrive and SharePoint).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client_id&amp;gt;&lt;/code&gt; / &lt;code&gt;client_secret&amp;gt;&lt;/code&gt;: Press Enter to leave blank (uses default application credentials).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;region&amp;gt;&lt;/code&gt;: Enter &lt;code&gt;1&lt;/code&gt; (Microsoft Cloud Global).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tenant&amp;gt;&lt;/code&gt;: Press Enter to leave blank (not needed for interactive personal OAuth).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Edit advanced config?&amp;gt;&lt;/code&gt;: Enter &lt;code&gt;n&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Use web browser to automatically authenticate?&amp;gt;&lt;/code&gt;: Enter &lt;code&gt;y&lt;/code&gt;. A browser tab will open automatically. Sign in with your E5 tenant credentials and grant the requested permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Select the Target SharePoint Site&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Return to the terminal after successful browser authorisation. When prompted for the storage type, enter &lt;code&gt;2&lt;/code&gt; (SharePoint site).&lt;/li&gt;
&lt;li&gt;Rclone will list all SharePoint sites in your tenant. Enter the corresponding numerical index for the communication site created in &lt;a href=&quot;#0-create-a-communication-site-in-sharepoint&quot;&gt;Section 0&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Select the document library by entering the numerical index.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Confirm the Base Remote&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Review the configuration summary and enter &lt;code&gt;y&lt;/code&gt; to confirm.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 2: Create the Encrypted Overlay Remote (&lt;code&gt;sp-crypt&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;Overlay the base SharePoint remote with Rclone’s native client-side encryption:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;In the &lt;code&gt;rclone config&lt;/code&gt; menu, enter &lt;code&gt;n&lt;/code&gt; to create a second remote.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Name the remote &lt;code&gt;sp-crypt&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;code&gt;Storage&amp;gt;&lt;/code&gt;, enter &lt;code&gt;crypt&lt;/code&gt; (or select the number for &lt;strong&gt;Encrypt/Decrypt a remote&lt;/strong&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;code&gt;remote&amp;gt;&lt;/code&gt;, specify the base remote and target storage folder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sp:vault&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;This stores all encrypted blobs inside a dedicated &lt;code&gt;vault&lt;/code&gt; folder in your SharePoint document library, leaving the rest of your SharePoint available for regular, unencrypted media files.&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;code&gt;filename_encryption&amp;gt;&lt;/code&gt;, enter &lt;code&gt;1&lt;/code&gt; (&lt;strong&gt;Standard&lt;/strong&gt;) to fully encrypt filenames into randomized alphanumeric strings.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;code&gt;directory_name_encryption&amp;gt;&lt;/code&gt;, enter &lt;code&gt;true&lt;/code&gt; (or &lt;code&gt;1&lt;/code&gt;) to encrypt directory names.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;code&gt;password&amp;gt;&lt;/code&gt;, enter &lt;code&gt;g&lt;/code&gt; to &lt;strong&gt;generate a random password&lt;/strong&gt; (recommended over a manual password for maximum cryptographic security):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When prompted for &lt;code&gt;Password strength in bits&lt;/code&gt;, enter &lt;code&gt;128&lt;/code&gt; or &lt;code&gt;1024&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;y&lt;/code&gt; to confirm the generated password, and immediately save it in your password manager.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;(Alternatively, enter &lt;code&gt;y&lt;/code&gt; if you prefer to type in your own passphrase).&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For &lt;code&gt;password2&amp;gt;&lt;/code&gt; (salt), enter &lt;code&gt;g&lt;/code&gt; to generate a random salt phrase as well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When prompted for bits, enter &lt;code&gt;128&lt;/code&gt; or &lt;code&gt;1024&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;y&lt;/code&gt; to confirm, and save the salt alongside your password in your password manager.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;(Alternatively, enter &lt;code&gt;y&lt;/code&gt; to type a custom salt, or &lt;code&gt;n&lt;/code&gt; to skip, though adding a salt is strongly recommended).&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Press Enter to skip advanced configuration, review the summary, enter &lt;code&gt;y&lt;/code&gt; to save, and enter &lt;code&gt;q&lt;/code&gt; to quit the wizard.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;p&gt;CAUTION&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backup Your Password and Salt&lt;/strong&gt;: Rclone uses standard, zero-knowledge encryption (XSalsa20 + Poly1305). There is no password recovery or reset mechanism. Store both your password and salt safely in a password manager. With these two keys, you can decrypt and access your files on any Mac, Linux, or Windows system.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Manual Mount &amp;amp; Connectivity Test (Optional)&lt;/h2&gt;
&lt;p&gt;Configuring the local Virtual File System (VFS) cache provides seamless on-demand access: files are listed in Finder at their full remote sizes, while local cache files are allocated sparsely so that only read or written byte ranges consume SSD storage. Cached chunks remain available for immediate re-access and are automatically evicted by age (&lt;code&gt;--vfs-cache-max-age 12h&lt;/code&gt;) or size limits (&lt;code&gt;--vfs-cache-max-size 50G&lt;/code&gt;).&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;This section is intended for manually testing whether the virtual drive mounts and operates correctly. If you prefer to configure Rclone directly as a persistent background service that starts automatically at login, you can verify your mount here and proceed to &lt;a href=&quot;#4-configure-automated-startup-at-login-macos-launchctl&quot;&gt;Section 4&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Step 1: Create the Local Mount Points&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/SharePoint&lt;/span&gt;&lt;span&gt; ~/SharePointVault&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Execute the Mount Command&lt;/h3&gt;
&lt;p&gt;Run the following optimised mount command in the foreground to test connectivity and review terminal logs for the unencrypted media mount:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/opt/local/bin/rclone&lt;/span&gt;&lt;span&gt; mount&lt;/span&gt;&lt;span&gt; sp:&lt;/span&gt;&lt;span&gt; ~/SharePoint&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --vfs-cache-mode&lt;/span&gt;&lt;span&gt; full&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --vfs-cache-max-age&lt;/span&gt;&lt;span&gt; 12h&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --vfs-cache-max-size&lt;/span&gt;&lt;span&gt; 50G&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --vfs-cache-poll-interval&lt;/span&gt;&lt;span&gt; 1m&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --poll-interval&lt;/span&gt;&lt;span&gt; 1m&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --vfs-write-back&lt;/span&gt;&lt;span&gt; 5s&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --onedrive-chunk-size&lt;/span&gt;&lt;span&gt; 125M&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --buffer-size&lt;/span&gt;&lt;span&gt; 64M&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  --volname&lt;/span&gt;&lt;span&gt; &quot;SharePoint&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;To test mounting the encrypted vault instead, substitute &lt;code&gt;sp:&lt;/code&gt; with &lt;code&gt;sp-crypt:&lt;/code&gt;, &lt;code&gt;~/SharePoint&lt;/code&gt; with &lt;code&gt;~/SharePointVault&lt;/code&gt;, and &lt;code&gt;--volname &quot;SharePoint&quot;&lt;/code&gt; with &lt;code&gt;--volname &quot;SharePointVault&quot;&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;Running in the foreground (without &lt;code&gt;--daemon&lt;/code&gt;) lets you inspect real-time log output, verify connectivity, and confirm that the filesystem mounts properly. Once you have confirmed that the mount functions as expected, press &lt;code&gt;Ctrl + C&lt;/code&gt; in Terminal to cleanly terminate the test run, then proceed to &lt;a href=&quot;#4-configure-automated-startup-at-login-macos-launchctl&quot;&gt;Section 4&lt;/a&gt; to set up persistent background startup for both drives.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Key Parameters Explained&lt;/h3&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Parameter&lt;/th&gt;&lt;th&gt;Core Behavior &amp;amp; Purpose&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--vfs-cache-mode full&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Enables a comprehensive cache layer. Allows sequential and random access (seeking) on large videos without data corruption.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--vfs-cache-max-age 12h&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Automatic Space Freeing: Deletes the local SSD cache of any file that has not been read or written to for 12 hours, returning local disk consumption to zero.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--vfs-cache-max-size 50G&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Caps maximum local cache size. Cleans older cached chunks using an LRU (least-recently-used) policy if this threshold is reached.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--vfs-cache-poll-interval 1m&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Scans the local cache directory once per minute to evict expired or overflowing data promptly.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--poll-interval 1m&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Delta Polling: Queries Microsoft Graph every minute for changes, ensuring additions or deletions made in the cloud reflect in Finder promptly.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--vfs-write-back 5s&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Delays upload until 5 seconds after a file is closed, preventing lockups caused by simultaneous writing and uploading.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--onedrive-chunk-size 125M&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Increases upload chunk size to 125MB (a multiple of 320KiB required by Microsoft’s API), optimizing throughput on high-speed internet.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--buffer-size 64M&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Allocates a 64MB read-ahead buffer in RAM for each open file to absorb network latency fluctuations during video playback.&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--volname &quot;SharePoint&quot;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Displays the mount as an external drive named “SharePoint” on your desktop and Finder sidebar.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transparent Decryption &amp;amp; Universal Streaming Access&lt;/strong&gt;:
In &lt;code&gt;~/SharePointVault&lt;/code&gt;, Rclone decrypts files dynamically in RAM-presenting normal filenames and data with zero manual steps. In &lt;code&gt;~/SharePoint&lt;/code&gt;, files are uploaded unencrypted, allowing you to stream or download large videos on any device (such as mobile phones, smart TVs, or web browsers) without needing Rclone or encryption keys.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Exploring Remotes with Rclone Web (Optional)&lt;/h3&gt;
&lt;p&gt;The modern official web interface &lt;strong&gt;&lt;a href=&quot;https://github.com/rclone/rclone-web&quot;&gt;Rclone Web&lt;/a&gt;&lt;/strong&gt; is bundled directly into latest Rclone releases. If you ever want to visually inspect your configured cloud remotes (&lt;code&gt;sp&lt;/code&gt; and &lt;code&gt;sp-crypt&lt;/code&gt;) or explore cloud files in a web browser without mounting:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;rclone&lt;/span&gt;&lt;span&gt; gui&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Instant Browser Launch&lt;/strong&gt;: Automatically starts a temporary web GUI server and opens your default browser pre-authenticated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-Demand Inspection&lt;/strong&gt;: When you are finished exploring, press &lt;code&gt;Ctrl + C&lt;/code&gt; in Terminal to terminate the web GUI.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Configure Automated Startup at Login (macOS Launchctl)&lt;/h2&gt;
&lt;p&gt;Use macOS’s native &lt;code&gt;launchctl&lt;/code&gt; service to maintain persistent, background mounting for both virtual drives upon system login.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;launchctl&lt;/code&gt; is the ideal service manager on macOS&lt;/strong&gt;:
macOS &lt;code&gt;launchctl&lt;/code&gt; manages both virtual drives as native user daemons. It monitors both processes silently, consumes minimal memory, and automatically relaunches a mount if an unexpected network disruption occurs.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Step 1: Generate the LaunchAgent Configurations&lt;/h3&gt;
&lt;h4&gt;1. Unencrypted Media Mount Service (&lt;code&gt;com.user.rclone.sharepoint.plist&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;Run the following command to create the directory structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/SharePoint&lt;/span&gt;&lt;span&gt; ~/SharePointVault&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/Library/LaunchAgents&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/.config/rclone&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the unencrypted media drive mount service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt; EOF&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; ~/Library/LaunchAgents/com.user.rclone.sharepoint.plist&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;plist version=&quot;1.0&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;string&amp;gt;com.user.rclone.sharepoint&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;EnvironmentVariables&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;key&amp;gt;PATH&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;array&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;/opt/local/bin/rclone&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;mount&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;sp:&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/SharePoint&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-cache-mode&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;full&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-cache-max-age&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;12h&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-cache-max-size&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;50G&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-cache-poll-interval&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;1m&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--poll-interval&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;1m&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-write-back&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;5s&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--onedrive-chunk-size&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;125M&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--buffer-size&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;64M&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--volname&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;SharePoint&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/array&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;true/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;KeepAlive&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;key&amp;gt;SuccessfulExit&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;false/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;ThrottleInterval&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;integer&amp;gt;10&amp;lt;/integer&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;string&amp;gt;&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/.config/rclone/sharepoint-mount.log&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;string&amp;gt;&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/.config/rclone/sharepoint-mount.log&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. Encrypted Vault Mount Service (&lt;code&gt;com.user.rclone.sharepointvault.plist&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;Run the following command to create the encrypted zero-knowledge vault mount service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt; EOF&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; ~/Library/LaunchAgents/com.user.rclone.sharepointvault.plist&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;plist version=&quot;1.0&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;string&amp;gt;com.user.rclone.sharepointvault&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;EnvironmentVariables&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;key&amp;gt;PATH&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;array&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;/opt/local/bin/rclone&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;mount&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;sp-crypt:&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/SharePointVault&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-cache-mode&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;full&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-cache-max-age&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;12h&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-cache-max-size&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;30G&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-cache-poll-interval&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;1m&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--poll-interval&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;1m&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--vfs-write-back&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;5s&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--onedrive-chunk-size&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;125M&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--buffer-size&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;64M&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;--volname&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;string&amp;gt;SharePointVault&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/array&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;true/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;KeepAlive&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;key&amp;gt;SuccessfulExit&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;false/&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;/dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;ThrottleInterval&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;integer&amp;gt;10&amp;lt;/integer&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;string&amp;gt;&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/.config/rclone/sharepointvault-mount.log&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;string&amp;gt;&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/.config/rclone/sharepointvault-mount.log&amp;lt;/string&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;PATH&lt;/code&gt;&lt;/strong&gt;: Ensures auxiliary tools and user-space helper symlinks in &lt;code&gt;/usr/local/bin&lt;/code&gt; can be resolved by background jobs.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Step 2: Activate the Services&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Ensure both target mount points are clean and unmounted&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;diskutil&lt;/span&gt;&lt;span&gt; unmount&lt;/span&gt;&lt;span&gt; force&lt;/span&gt;&lt;span&gt; ~/SharePoint&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; umount&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; ~/SharePoint&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;diskutil&lt;/span&gt;&lt;span&gt; unmount&lt;/span&gt;&lt;span&gt; force&lt;/span&gt;&lt;span&gt; ~/SharePointVault&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; umount&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; ~/SharePointVault&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Bootstrap and start background services in the modern user GUI domain&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;launchctl&lt;/span&gt;&lt;span&gt; bootstrap&lt;/span&gt;&lt;span&gt; gui/&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;~/Library/LaunchAgents/com.user.rclone.sharepoint.plist&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;launchctl&lt;/span&gt;&lt;span&gt; bootstrap&lt;/span&gt;&lt;span&gt; gui/&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;~/Library/LaunchAgents/com.user.rclone.sharepointvault.plist&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both the &lt;strong&gt;SharePoint&lt;/strong&gt; and &lt;strong&gt;SharePointVault&lt;/strong&gt; drives will now automatically mount in Finder upon every login.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;~/SharePoint&lt;/code&gt; mounts the root of your SharePoint document library, the &lt;code&gt;vault/&lt;/code&gt; folder will appear inside &lt;code&gt;~/SharePoint&lt;/code&gt; containing encrypted hashes.
&lt;strong&gt;Do not edit, rename, or write files directly into &lt;code&gt;~/SharePoint/vault&lt;/code&gt;&lt;/strong&gt;. Always interact with your encrypted files through the dedicated &lt;code&gt;~/SharePointVault&lt;/code&gt; mount point.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;strong&gt;Dual-Mount Architecture&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Local Mac (Finder):&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── ~/SharePoint         ──(FUSE-T)──&amp;gt;  sp:        (Unencrypted: Videos, Media, Public files)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;└── ~/SharePointVault    ──(FUSE-T)──&amp;gt;  sp-crypt:  (Encrypted: Private documents &amp;amp; backups)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Microsoft SharePoint (Cloud):&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── Videos/              (Plain unencrypted files — downloadable anywhere)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;├── Documents/           (Plain unencrypted files)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;└── vault/               (Encrypted ciphertext blobs — managed by sp-crypt)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If the drive icons do not appear on your Desktop or Finder sidebar&lt;/strong&gt;:
FUSE-T mounts the drives as network filesystems (NFS). Ensure macOS allows displaying connected network volumes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Finder&lt;/strong&gt; → press &lt;code&gt;Cmd + ,&lt;/code&gt; (&lt;strong&gt;Settings&lt;/strong&gt; / &lt;strong&gt;Preferences&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Under the &lt;strong&gt;General&lt;/strong&gt; tab, check &lt;strong&gt;Connected servers&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under the &lt;strong&gt;Sidebar&lt;/strong&gt; tab, ensure &lt;strong&gt;Connected servers&lt;/strong&gt; is checked under &lt;strong&gt;Locations&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;Alternatively, navigate to &lt;code&gt;~/SharePoint&lt;/code&gt; and &lt;code&gt;~/SharePointVault&lt;/code&gt; in Finder and drag both folders directly into your &lt;strong&gt;Favorites&lt;/strong&gt; sidebar for one-click access.&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Maintenance and Operations&lt;/h2&gt;
&lt;h3&gt;Manually Free All Local Space Immediately&lt;/h3&gt;
&lt;div&gt;
&lt;p&gt;WARNING&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data Loss Risk&lt;/strong&gt;: Before running this command, verify that all files have finished uploading to the cloud. You can monitor log files (&lt;code&gt;tail -f ~/.config/rclone/sharepoint-mount.log&lt;/code&gt;) to confirm transfer queues are empty, or check Activity Monitor to ensure &lt;code&gt;rclone&lt;/code&gt; network egress has dropped to zero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Never clear this directory while uploads are in progress&lt;/strong&gt;, as files queued in the local buffer will be permanently erased before reaching the cloud, causing irrecoverable data loss or corrupted remote files.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;To instantly wipe the local cache without affecting cloud files, delete the local cache and metadata folders:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;rm&lt;/span&gt;&lt;span&gt; -rf&lt;/span&gt;&lt;span&gt; ~/Library/Caches/rclone/vfs&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Safe Unmounting&lt;/h3&gt;
&lt;p&gt;Do not drag the mounted volume to the Trash. Unmount according to how the drive was launched:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;If running via Launchctl background service (Section 4)&lt;/strong&gt;:
Boot out the background services directly. This terminates Rclone cleanly and automatically unmounts both volumes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;launchctl&lt;/span&gt;&lt;span&gt; bootout&lt;/span&gt;&lt;span&gt; gui/&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;/com.user.rclone.sharepoint&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;launchctl&lt;/span&gt;&lt;span&gt; bootout&lt;/span&gt;&lt;span&gt; gui/&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;/com.user.rclone.sharepointvault&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;If running manually via Terminal (Section 3)&lt;/strong&gt;:
Unmount the mount points directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;diskutil&lt;/span&gt;&lt;span&gt; unmount&lt;/span&gt;&lt;span&gt; ~/SharePoint&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; umount&lt;/span&gt;&lt;span&gt; ~/SharePoint&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;diskutil&lt;/span&gt;&lt;span&gt; unmount&lt;/span&gt;&lt;span&gt; ~/SharePointVault&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; umount&lt;/span&gt;&lt;span&gt; ~/SharePointVault&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(If a mount point remains busy, force unmount with &lt;code&gt;diskutil unmount force ~/SharePoint&lt;/code&gt; or &lt;code&gt;diskutil unmount force ~/SharePointVault&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Inspecting and Truncating Logs&lt;/h3&gt;
&lt;p&gt;By default, Rclone runs at the &lt;code&gt;NOTICE&lt;/code&gt; logging level, keeping log file growth negligible (typically under 1MB per year).&lt;/p&gt;
&lt;p&gt;To view live log output in Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# View unencrypted media mount logs:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;tail&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; ~/.config/rclone/sharepoint-mount.log&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# View encrypted vault mount logs:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;tail&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; ~/.config/rclone/sharepointvault-mount.log&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you ever wish to instantly truncate and free log file space without restarting the background service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; ~/.config/rclone/sharepoint-mount.log&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; ~/.config/rclone/sharepointvault-mount.log&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Managing AppleDouble (&lt;code&gt;._*&lt;/code&gt;) and &lt;code&gt;.DS_Store&lt;/code&gt; Companion Files&lt;/h3&gt;
&lt;p&gt;When copying files with extended attributes (such as quarantine flags from browser downloads, AirDrop metadata, or Finder tags) to a virtual or network filesystem, macOS automatically generates companion metadata files prefixed with &lt;code&gt;._&lt;/code&gt; (AppleDouble format).&lt;/p&gt;
&lt;p&gt;Understanding how macOS and Rclone handle these companion files resolves common sync puzzles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Disable &lt;code&gt;.DS_Store&lt;/code&gt; Generation on Network Stores (System Optimisation)&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;Run this native macOS command to instruct Finder never to create &lt;code&gt;.DS_Store&lt;/code&gt; files on network shares and FUSE mounts, then restart Finder to apply the change immediately:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;defaults&lt;/span&gt;&lt;span&gt; write&lt;/span&gt;&lt;span&gt; com.apple.desktopservices&lt;/span&gt;&lt;span&gt; DSDontWriteNetworkStores&lt;/span&gt;&lt;span&gt; -bool&lt;/span&gt;&lt;span&gt; TRUE&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;killall&lt;/span&gt;&lt;span&gt; Finder&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can verify that the setting took effect (should output &lt;code&gt;1&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;defaults&lt;/span&gt;&lt;span&gt; read&lt;/span&gt;&lt;span&gt; com.apple.desktopservices&lt;/span&gt;&lt;span&gt; DSDontWriteNetworkStores&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DSDontWriteNetworkStores&lt;/code&gt; exclusively suppresses &lt;code&gt;.DS_Store&lt;/code&gt;. It has zero effect on AppleDouble (&lt;code&gt;._*&lt;/code&gt;) files, which macOS treats as essential file metadata forks.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;--exclude&lt;/code&gt; is Omitted &amp;amp; Preventing &lt;code&gt;._*&lt;/code&gt; Companion Files&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;rclone mount&lt;/code&gt;, &lt;code&gt;--exclude&lt;/code&gt; acts purely as a &lt;strong&gt;read/visibility filter&lt;/strong&gt;, it hides files from Finder, but it does &lt;em&gt;not&lt;/em&gt; intercept or block files written into the local VFS mount by macOS. Adding &lt;code&gt;--exclude &quot;._*&quot;&lt;/code&gt; creates an illusion: Finder writes &lt;code&gt;._filename&lt;/code&gt;, Rclone uploads it to SharePoint anyway, and then hides it from your local view so you cannot even see or delete it with normal &lt;code&gt;rm&lt;/code&gt; commands.&lt;/p&gt;
&lt;p&gt;Omitting &lt;code&gt;--exclude&lt;/code&gt; keeps your local view completely consistent with cloud storage. To actually stop macOS from generating and uploading &lt;code&gt;._*&lt;/code&gt; companion files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Strip Extended Attributes Before Copying&lt;/strong&gt;: Remove metadata (quarantine, tags) so macOS sees clean data files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Single file or folder:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xattr&lt;/span&gt;&lt;span&gt; -c&lt;/span&gt;&lt;span&gt; &quot;filename.jpg&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Entire directory recursively:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;xattr&lt;/span&gt;&lt;span&gt; -cr&lt;/span&gt;&lt;span&gt; /path/to/folder&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Copy via Terminal without Extended Attributes&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; -X&lt;/span&gt;&lt;span&gt; &quot;filename.jpg&quot;&lt;/span&gt;&lt;span&gt; ~/SharePoint/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Or to the encrypted vault:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; -X&lt;/span&gt;&lt;span&gt; &quot;filename.jpg&quot;&lt;/span&gt;&lt;span&gt; ~/SharePointVault/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# For directories:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; -RX&lt;/span&gt;&lt;span&gt; /path/to/folder&lt;/span&gt;&lt;span&gt; ~/SharePoint/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Purge Stuck Companion Files from Local Cache&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;If your current transfer queue is stuck retrying &lt;code&gt;._*&lt;/code&gt; or &lt;code&gt;.DS_Store&lt;/code&gt; files, delete them from the local cache to allow legitimate files to proceed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt; ~/Library/Caches/rclone/vfs&lt;/span&gt;&lt;span&gt; -name&lt;/span&gt;&lt;span&gt; &quot;._*&quot;&lt;/span&gt;&lt;span&gt; -delete&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt; ~/Library/Caches/rclone/vfs&lt;/span&gt;&lt;span&gt; -name&lt;/span&gt;&lt;span&gt; &quot;.DS_Store&quot;&lt;/span&gt;&lt;span&gt; -delete&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt; ~/Library/Caches/rclone/vfsMeta&lt;/span&gt;&lt;span&gt; -name&lt;/span&gt;&lt;span&gt; &quot;._*&quot;&lt;/span&gt;&lt;span&gt; -delete&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt; ~/Library/Caches/rclone/vfsMeta&lt;/span&gt;&lt;span&gt; -name&lt;/span&gt;&lt;span&gt; &quot;.DS_Store&quot;&lt;/span&gt;&lt;span&gt; -delete&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Purge Existing Companion Files from Cloud Storage&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;To mass-delete any lingering &lt;code&gt;._*&lt;/code&gt; and &lt;code&gt;.DS_Store&lt;/code&gt; files across both cloud spaces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# From unencrypted SharePoint storage:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;rclone&lt;/span&gt;&lt;span&gt; delete&lt;/span&gt;&lt;span&gt; sp:&lt;/span&gt;&lt;span&gt; --include&lt;/span&gt;&lt;span&gt; &quot;._*&quot;&lt;/span&gt;&lt;span&gt; --include&lt;/span&gt;&lt;span&gt; &quot;.DS_Store&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# From encrypted vault:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;rclone&lt;/span&gt;&lt;span&gt; delete&lt;/span&gt;&lt;span&gt; sp-crypt:&lt;/span&gt;&lt;span&gt; --include&lt;/span&gt;&lt;span&gt; &quot;._*&quot;&lt;/span&gt;&lt;span&gt; --include&lt;/span&gt;&lt;span&gt; &quot;.DS_Store&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Preventing SharePoint Version Bloat (Critical)&lt;/h3&gt;
&lt;div&gt;
&lt;p&gt;WARNING&lt;/p&gt;
&lt;p&gt;To prevent minor file modifications or metadata changes on large videos from consuming the 1.24TB pool through version history, adjust document versioning settings on your Communication site &lt;em&gt;(as noted in &lt;a href=&quot;#0-create-a-communication-site-in-sharepoint&quot;&gt;Section 0&lt;/a&gt;, “No versioning” is only available on Communication sites)&lt;/em&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Navigate to your Communication site document library in a web browser.&lt;/li&gt;
&lt;li&gt;Click the gear icon (&lt;strong&gt;Settings&lt;/strong&gt;) → &lt;strong&gt;Library settings&lt;/strong&gt; → &lt;strong&gt;More library settings&lt;/strong&gt; → &lt;strong&gt;Versioning settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Document Version History&lt;/strong&gt;, select &lt;strong&gt;No versioning&lt;/strong&gt; and click &lt;strong&gt;OK&lt;/strong&gt; at the bottom.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-09-21/versioning-settings.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Architectural Trade-offs &amp;amp; Ideal Workflows&lt;/h2&gt;
&lt;p&gt;You’ve finally broken free from OneDrive’s sickeningly broken behaviour of SharePoint on macOS, a disaster born from corporate politics between Apple and Microsoft. With this dual-mount architecture in place, here is what you’ve gained:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Dual-Mount Flexibility &amp;amp; Universal Media Access&lt;/strong&gt;:
By mounting the raw remote &lt;code&gt;sp:&lt;/code&gt; to &lt;code&gt;~/SharePoint&lt;/code&gt;, large video files and media collections remain unencrypted in the cloud. You can stream them seamlessly with byte-range requests and instant seeking on macOS (via players like IINA, Infuse, or VLC), but you can &lt;em&gt;also&lt;/em&gt; download or play them when away from your Mac—using the SharePoint web interface, OneDrive mobile app, or smart TV players—without needing Rclone or encryption keys.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero-Knowledge Privacy &amp;amp; Metadata Immunity in &lt;code&gt;~/SharePointVault&lt;/code&gt;&lt;/strong&gt;:
Sensitive personal records, credentials, and backups placed into &lt;code&gt;~/SharePointVault&lt;/code&gt; are encrypted on the fly via &lt;code&gt;rclone crypt&lt;/code&gt; (XSalsa20 + Poly1305). Because SharePoint only receives opaque ciphertext blobs, it cannot crack open Office documents or PDFs to inject tenant UUIDs (&lt;code&gt;sizes differ&lt;/code&gt; loops), and cloud administrators or compromised credentials cannot inspect your files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eliminating macOS Sync Deadlocks&lt;/strong&gt;:
Bypasses Apple’s &lt;code&gt;fileproviderd&lt;/code&gt; architecture completely, eliminating circular upload freezes, 0% progress bugs, and high CPU lockups during large transfers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;True Cloud Capacity&lt;/strong&gt;:
Unlocks the tenant-wide SharePoint storage pool, bypassing Microsoft’s strict 10GB personal OneDrive quota on Developer E5 accounts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Elimination of Filename Character Restrictions in the Vault&lt;/strong&gt;:
SharePoint strictly rejects characters like &lt;code&gt;&quot; * : &amp;lt; &amp;gt; ? / \ |&lt;/code&gt;, leading/trailing spaces, and periods at the end of filenames. Inside &lt;code&gt;~/SharePointVault&lt;/code&gt;, &lt;code&gt;rclone crypt&lt;/code&gt; encrypts all filenames into standard alphanumeric hashes, ensuring every valid macOS filename is supported without errors.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;However, to ensure optimal performance, keep this in mind:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;High Overhead on Thousands of Small Files&lt;/strong&gt;:
Uploading a single 10GB video requires one continuous stream that saturates available network bandwidth. In contrast, copying a directory containing 30,000 fragmented files (such as &lt;code&gt;node_modules&lt;/code&gt; or unpacked game assets) requires tens of thousands of individual REST API calls to Microsoft Graph. This creates severe network round-trip latency and quickly triggers &lt;strong&gt;HTTP 429 (Too Many Requests)&lt;/strong&gt; rate limiting from Microsoft.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Golden Rule: Large Files Directly, Small Files Zipped&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Directly to Mount (&lt;code&gt;~/SharePoint&lt;/code&gt; or &lt;code&gt;~/SharePointVault&lt;/code&gt;)&lt;/strong&gt;: Videos, TV series, photo libraries, disc images (&lt;code&gt;.iso&lt;/code&gt;, &lt;code&gt;.dmg&lt;/code&gt;), virtual machine disks, and pre-packaged archives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zip First Locally&lt;/strong&gt;: Code repositories, game installation directories, emulator ROM collections, and folders containing thousands of small files or HTML manuals. Compress them into a single &lt;code&gt;.zip&lt;/code&gt; or &lt;code&gt;.7z&lt;/code&gt; file before moving them to the mount. This avoids API rate limiting, preserves byte-for-byte integrity, and guarantees maximum upload throughput.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Clinging to Life on an Obsolete Intel Mac</title><link>https://michifumi.de/blog/2026-08-30-clinging-to-life-on-an-obsolete-intel-mac/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-08-30-clinging-to-life-on-an-obsolete-intel-mac/</guid><description>Dealing with hardware obsolescence and Homebrew tier 3 software hurdles on an Intel MacBook in late 2026.</description><pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Dealing with hardware obsolescence and Homebrew tier 3 software hurdles on an Intel MacBook in late 2026.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;On 13 August 2026, Apple listed my old Intel MacBook as an obsolete product. According to the &lt;a href=&quot;https://support.apple.com/en-ie/102772&quot;&gt;official description&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Products are considered obsolete when Apple stopped distributing them for sale more than 7 years ago.&lt;/p&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;p&gt;Apple discontinues all hardware service for obsolete products, and service providers cannot order parts for obsolete products.&lt;/p&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That means that, except for the battery, I cannot get any repairs from official support. I would only be able to try my luck at a third-party repair shop if this old fellow breaks again.&lt;/p&gt;
&lt;p&gt;Over the years, I’ve spilt milk on it and had to get the motherboard repaired. I dropped it at Connolly Station, thought it was definitely broken, and I literally held my head in my hands on the bench for several minutes. But this old chap has pulled through time and time again. Thanks to its 32 GB RAM, I can at least dip my toes into some small-size LLMs on it. While the lack of Apple Silicon’s unified memory architecture for GPU acceleration and modern ML frameworks like MLX is really a pain in the ass, considering the recent price of RAM, I need it to keep chugging along for a couple more years.&lt;/p&gt;
&lt;p&gt;The problem is, it’s not just the lack of hardware repairs, the fading software support is coming back to bite me in the ass too. I even started seeing this issue when I updated using Homebrew:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Error: node: no bottle available!&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;If you&apos;re feeling brave, you can try to install from source with:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  brew install --build-from-source node&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;This is a Tier 3 configuration:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  https://docs.brew.sh/Support-Tiers#tier-3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means they are withdrawing support for the pre-built bottles for my old fellow. Although I could download the source code and compile it myself, that would mean more work. And the last thing I want is any hassle.
Luckily, I found a workaround. I replaced the current one with node@24 LTS.
&lt;a href=&quot;#the-redemption&quot;&gt;Click&lt;/a&gt; to ignore Homebrew and check out the truly useful masterpiece!&lt;/p&gt;
&lt;p&gt;Replacing it is quite simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install Node.js 24 LTS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; node@24&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Uninstall the unversioned formula:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; uninstall&lt;/span&gt;&lt;span&gt; node&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Link &lt;code&gt;node@24&lt;/code&gt; and overwrite any remaining npm files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; link&lt;/span&gt;&lt;span&gt; --overwrite&lt;/span&gt;&lt;span&gt; node@24&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Verify the active versions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;node&lt;/span&gt;&lt;span&gt; -v&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; -v&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;According to &lt;a href=&quot;https://formulae.brew.sh/formula/node@24&quot;&gt;Homebrew&lt;/a&gt;, this allows me to cling to life until 30 April 2027. I hope I can afford a new Mac before that time comes, otherwise, this kind of issue will become more and more common in the foreseeable future.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;npm does not age out unused-but-valid packages. The &lt;a href=&quot;https://docs.npmjs.com/cli/v12/commands/npm-cache&quot;&gt;docs&lt;/a&gt; are explicit: the cache grows as you install new packages, and npm will not prune it on its own.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;To clean up corrupted or orphaned cache entries and verify integrity without wiping everything, you can run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; cache&lt;/span&gt;&lt;span&gt; verify&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This garbage-collects unneeded index records and repairs corrupted data, though because it preserves all valid cached packages, it won’t free up space taken by older packages you no longer need.&lt;/p&gt;
&lt;p&gt;If you are severely low on disk space and truly want to purge all cached packages, the only official way is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; cache&lt;/span&gt;&lt;span&gt; clean&lt;/span&gt;&lt;span&gt; --force&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use this sparingly, however, wiping valid cache entries forces npm to re-download dependencies from the registry on future installs, wasting bandwidth and slowing them down.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Redemption&lt;/h2&gt;
&lt;p&gt;Several days later, another message showed up when I used brew upgrade.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Warning: You are using macOS on Intel x86_64.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;We do not provide support for this platform (as-of September 2026, announced August 2025).&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Apple have dropped Intel x86_64 support in macOS Golden Gate (27).&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;GitHub Actions are dropping macOS Intel x86_64 runners in 2027.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Homebrew is a non-profit project run entirely by volunteers, not employees.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;If the biggest companies in the world cannot support macOS Intel x86_64&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;any longer, sadly neither can we.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;You will have better luck with MacPorts which still supports macOS Intel x86_64:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  https://www.macports.org&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I never imagined that Homebrew’s decision to discontinue support for Intel Macs and introduce me to &lt;a href=&quot;https://ports.macports.org/&quot;&gt;MacPorts&lt;/a&gt; would open up a whole new world for me, to the extent that I ended up completely overhauling my development environment. It all started with &lt;a href=&quot;https://ports.macports.org/port/nodejs26/details/&quot;&gt;this note&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;nodejs26 does not contain npm but it can be installed as a separate port. Pick from the choices listed by running:
port search —name —glob ‘npm*’&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I was tired of npm’s cache and node_modules, which are located in each project root folder, eating up a lot of my storage for a long time. But no matter whether I installed Node.js from the &lt;a href=&quot;https://nodejs.org/en/download&quot;&gt;official website&lt;/a&gt; or Homebrew, npm always came bundled with Node.js. I now know that I can build it from source with only the runtime. However, since there are no instructions on the official landing page or docs, it was hard to realise there was a distinction between the runtime and the package manager back then.&lt;/p&gt;
&lt;p&gt;It was then that I quite naturally came across &lt;a href=&quot;https://pnpm.io/&quot;&gt;pnpm&lt;/a&gt;.
&lt;strong&gt;pnpm&lt;/strong&gt; is a fast, disk-efficient package manager for the JavaScript ecosystem, designed as a drop-in replacement for npm.&lt;/p&gt;
&lt;p&gt;Compared to npm, its key advantages include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Massive Disk Space Savings:&lt;/strong&gt; It uses a global content-addressable store on your hard drive, hard-linking packages so the same dependency is never duplicated across projects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Significantly Faster Installs:&lt;/strong&gt; By skipping redundant downloads and running tasks concurrently, installs and CI builds are noticeably faster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict, Phantom-Dependency Prevention:&lt;/strong&gt; Unlike npm’s flattened &lt;code&gt;node_modules&lt;/code&gt;, pnpm uses symlinks to create a strict structure, preventing your code from accidentally importing dependencies you never explicitly declared in &lt;code&gt;package.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First-Class Monorepo Support:&lt;/strong&gt; It natively handles multi-package workspaces with zero hassle.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means I don’t need to worry about running &lt;code&gt;npm cache verify&lt;/code&gt;, &lt;code&gt;npm cache clean --force&lt;/code&gt;, or deleting &lt;code&gt;node_modules&lt;/code&gt; manually from time to time to keep my projects consistent and clean. I immediately installed MacPorts, installed nodejs26 and pnpm through it without a second thought, and migrated all my JavaScript projects to pnpm. For me, the way pnpm runs is like magic.&lt;/p&gt;
&lt;p&gt;But as I ventured deeper down the rabbit hole, I found something even more shocking: I could even get rid of pnpm! Not by switching to another package manager like Yarn that could be paired with Node.js, but an actual runtime that can replace Node.js and a package manager all by itself! That’s right, with Deno or Bun, not only will you no longer have to worry about which package manager to choose, but you’ll also be able to enjoy a more cutting-edge and faster development environment! If you are interested in the differences between Node.js, Deno, and Bun, you can find a more useful comparison on this &lt;a href=&quot;https://blog.stackademic.com/javascript-runtime-battle-node-js-vs-deno-vs-bun-which-should-you-pick-d3e662a37c84&quot;&gt;page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In my own experience, I’ve already migrated one of my &lt;a href=&quot;https://github.com/Shawshank01/xAI-desktop&quot;&gt;JavaScript projects&lt;/a&gt; totally from Node.js to Deno. All I needed was to replace &lt;code&gt;express&lt;/code&gt;, &lt;code&gt;cors&lt;/code&gt;, &lt;code&gt;dotenv&lt;/code&gt;, and Node-specific &lt;code&gt;http&lt;/code&gt; constructs with native Deno APIs. However, while my other two projects focus on TypeScript and should have been better suited to a switch to Deno (given its native support for TypeScript), one uses Electron for its GUI and the other is this blog which relies heavily on Node.js via Astro. After careful consideration, I ultimately decided against migrating them, opting instead to switch to Node.js + pnpm.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-08-30/meme.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;strong&gt;A blessing in disguise&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The reason I didn’t try Bun is because Deno was already installed on my MacBook as a dependency of yt-dlp. Yep, that is the only reason. Now you know how lazy I am.&lt;/p&gt;</content:encoded></item><item><title>twitter.now: Score, Don&apos;t Ban</title><link>https://michifumi.de/blog/2026-08-27-twitter-now-score-dont-ban/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-08-27-twitter-now-score-dont-ban/</guid><description>How twitter.now shifts content moderation from bans to AI scoring, and why it feels so familiar.</description><pubDate>Thu, 27 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;How twitter.now shifts content moderation from bans to AI scoring, and why it feels so familiar.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;You might have noticed the recent buzz around &lt;a href=&quot;https://twitter.now/&quot;&gt;twitter.now&lt;/a&gt;. It almost feels like a Reanimation Jutsu, bringing the iconic blue bird back into the spotlight.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-08-27/twitter-now-landing-page.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;The landing page of twitter.now reviving the iconic blue bird aesthetic&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Beyond the legal drama and trademark disputes with Elon Musk, launching “just another microblogging platform” is rarely game-changing. What genuinely caught my eye is their fundamental shift in content governance philosophy:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Instead of deleting posts or banning accounts, they score them.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Through their VERA and Trust OS (&lt;a href=&quot;https://finance.biggo.com/news/d1bdbb1d-0309-4fa3-b7c1-e43513240236&quot;&gt;powered by Gemini&lt;/a&gt;), posts receive credibility and trust scores. Users simply adjust a dynamic slider to set their own threshold for what content reaches their feed.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-08-27/twitter-now-vera.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;VERA &amp;amp; Trust OS dynamic trust scoring and slider controls&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Seeing twitter.now’s architecture instantly brought back memories of my Master’s final project, &lt;a href=&quot;https://github.com/Shawshank01/social-threat-guardian&quot;&gt;&lt;strong&gt;Social Threat Guardian&lt;/strong&gt;&lt;/a&gt; (built with React, TypeScript, Express, Oracle DB, and DistilBERT, though models like DeBERTa-v3 or RoBERTa would be the go-to today). While the use cases differ, the core philosophy is strikingly aligned: both use continuous AI-driven 0–100 scoring, putting control and visibility into data-backed thresholds.&lt;/p&gt;
&lt;p&gt;The difference lies in where the engine lives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;twitter.now&lt;/strong&gt; is a native social platform where higher scores mean higher trust for feed curation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Social Threat Guardian&lt;/strong&gt; was an external intelligence and alerting engine. It ingested posts across major platforms, scored them on a 0–100 toxicity scale (where higher = more dangerous), mapped historical platform-wide threat trends, and triggered instant alerts when high-risk content matched user-defined keywords, like real names, identities, or location data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Back then, our biggest hurdles were the astronomical cost of fetching streaming data (X’s API pricing felt like robbery), and feedback from one of my mentors pointing out that our project had a blurry target audience, making it hard for everyday users to grasp the value of a standalone monitoring dashboard.&lt;/p&gt;
&lt;p&gt;Seeing twitter.now, I can’t help but marvel at how it has directly resolved the two biggest problems we faced back then, by building the platform itself instead of paying for external APIs, and tying the scoring engine directly into the core product to make it easier for users to understand.&lt;/p&gt;
&lt;p&gt;Perhaps that is why they have the confidence to take Musk to court and launch a $20/$40 subscription plan, whilst we ultimately never got beyond the stage of a final project.&lt;/p&gt;</content:encoded></item><item><title>I Won a Draw</title><link>https://michifumi.de/blog/2026-08-24-i-won-a-draw/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-08-24-i-won-a-draw/</guid><description>It won&apos;t let me retire, but at least it put me in a great mood for the day.</description><pubDate>Mon, 24 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;It won&apos;t let me retire, but at least it put me in a great mood for the day.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;So here’s the story: I received two promotional emails from AIB in February and May, respectively, and participated in both events.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-08-24/disney-visa-campaign-2026.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;This one is from February. I actually won the May draw, but couldn’t find that specific email.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Participation was quite simple. First, register online at &lt;a href=&quot;https://www.aib.ie/visapromotion&quot;&gt;aib.ie/visapromotion&lt;/a&gt; and answer a Disney-related question correctly. Then, spend with an AIB Visa debit or credit card: every transaction, whether in Euro or a foreign currency, earned one entry, capped at 10 entries per draw period. If I’m not mistaken, I didn’t even use up all 10 attempts.&lt;/p&gt;
&lt;p&gt;The biggest prize for both events was a multi-day activity pass to Disneyland, one for France and the other for London. Each draw would ultimately see six lucky winners take the top prize, whilst a further 100 less fortunate participants received a merchandise bundle.&lt;/p&gt;
&lt;p&gt;Apparently, I’m one of the 200 lucky dogs.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-08-24/winner-trip.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;This congratulations email arrived out of nowhere on an otherwise unremarkable midday.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;After waiting for 19 days, I finally received my Precious, delivered by UPS.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-08-24/disney-pack.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;A Lion King tote bag, brochure, two mugs, and four plush toys.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I couldn’t resist writing a blog post to record this, because I genuinely can’t remember the last time I won anything from a draw. It’s definitely a dandy day for me, thanks to AIB, VISA, and the Goddess of Probability.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The day got even better when I found out the mug change colour with hot water. After consulting Gemini, it transpires that this is a typical piece of Disney merchandise known as a heat-reveal (thermochromic) mug.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-08-24/cold-mug.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;At room temperature&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-08-24/hot-mug.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;Upon heating&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Well, at least it’s new to me, and the science behind it is pretty fascinating.&lt;/p&gt;
&lt;p&gt;According to Gemini, thermochromic (heat-sensitive) pigment works via a reversible physical and chemical reaction—microcapsules expand and become transparent when heated, then reset when cooled. Because it is a physical transition rather than a consumable chemical fuel, there is no fixed cycle countdown (like a battery).&lt;/p&gt;
&lt;p&gt;However, the coating degrades over time due to environmental factors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UV Light / Direct Sunlight:&lt;/strong&gt; The biggest killer of color-changing pigment is UV radiation, which breaks down the organic dyes and permanently locks the design in either its hot or cold state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thermal Degradation:&lt;/strong&gt; Extremely high temperatures (like microwave exposure or dry heating) permanently damage the microcapsules.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Friction &amp;amp; Washing:&lt;/strong&gt; Abrasive scrubbers or harsh dishwashing soaps will physically strip the outer protective glaze and ink layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So in the end, I decided to put it back in the box.&lt;/p&gt;
&lt;p&gt;It came to to me, my own, my love… my… precioussss!&lt;/p&gt;</content:encoded></item><item><title>Everyone should start using a better DNS</title><link>https://michifumi.de/blog/2026-05-26-everyone-should-start-using-a-better-dns/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-05-26-everyone-should-start-using-a-better-dns/</guid><description>Although GDPR acts as a silent guardian for your personal data, taking additional steps yourself is still important for online privacy and security.</description><pubDate>Wed, 03 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Although GDPR acts as a silent guardian for your personal data, taking additional steps yourself is still important for online privacy and security.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;Third year in Ireland.&lt;br /&gt;
Moved houses three times.&lt;br /&gt;
After the recent move, I received a lot of marketing SMS ads from Three.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-05-27/three_ads.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;This makes me wonder: Is Three tracking my location using the cell tower? It detected that I had moved to a new place in a residential area, assumed that I needed things for the new house, and sent me these messages.
This is just a conspiracy theory, and I have no way of proving whether my theory is correct. But it reminds me of writing this blog, which is to show you what &lt;a href=&quot;https://aws.amazon.com/route53/what-is-dns/&quot;&gt;DNS&lt;/a&gt; is and how your ISP can use it to know everything about you, and use this information to send you targeted marketing messages.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What is DNS&lt;/h2&gt;
&lt;p&gt;The Domain Name System (DNS) is a foundational protocol of the internet, serving as the primary mechanism for translating human-readable hostnames into machine-readable IP addresses. Think of it this way: instead of memorizing a string of numbers like 172.135.248.206, you only need to remember a meaningful phrase like ThisIsRían.com. The DNS server resolves that name into the correct IP address (172.135.248.206), guiding your browser directly to Rían’s digital front door.&lt;/p&gt;
&lt;p&gt;Generally speaking, this task is handled by your ISP by default. This means that your ISP has the technical visibility to observe every domain you visit, down to the exact second. While privacy frameworks like the EU GDPR and ePrivacy regulations forbid European ISPs from selling browsing metadata to advertisers or retaining web history indiscriminately, laws in many jurisdictions still mandate retaining subscriber IP allocation data for months or years. More crucially, unencrypted DNS leaves your lookups vulnerable to on-path monitoring. In the following sections, I will use Irish law and Vodafone as examples.&lt;/p&gt;
&lt;p&gt;While your ISP absolutely knows you visited a specific website, HTTPS encryption stops them from seeing everything you do on that site. With HTTPS, your data looks like this to your ISP:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;What your ISP CAN see&lt;/th&gt;&lt;th&gt;What your ISP CANNOT see&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;You visited &lt;code&gt;wikipedia.org&lt;/code&gt;&lt;/td&gt;&lt;td&gt;The specific article you read (&lt;code&gt;/wiki/Domain_Name_System&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;You visited &lt;code&gt;amazon.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;What you searched for, clicked on, or bought&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Exactly when you connected and disconnected&lt;/td&gt;&lt;td&gt;The passwords or credit card info you typed&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;How much data you downloaded/uploaded&lt;/td&gt;&lt;td&gt;The text of messages you sent on encrypted apps&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;For most people, that might not be a big deal. But if you truly don’t want your ISP to know that you’re visiting a home improvement website or a government agent to know that you were on Pornhub at 2 am, you should probably start using DoT or DoH provided by a legitimate DNS provider such as &lt;a href=&quot;https://quad9.net/&quot;&gt;Quad9&lt;/a&gt; instead of your ISP’s default DNS, and here’s why.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Jurisdictional Governance and Data Protection Regimes&lt;/h2&gt;
&lt;p&gt;The structural divergence between public recursive resolvers and commercial Internet Service Providers (ISPs) is rooted in their organizational mandates and the legal jurisdictions that govern them. These factors dictate how subscriber metadata is processed, logged, and shielded from external surveillance.&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Governance Attribute&lt;/th&gt;&lt;th&gt;Quad9 Public DNS&lt;/th&gt;&lt;th&gt;Vodafone Ireland Default DNS&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Legal Entity Structure&lt;/td&gt;&lt;td&gt;Not-for-Profit Foundation&lt;/td&gt;&lt;td&gt;Commercial Telecommunications Provider&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Corporate Domicile&lt;/td&gt;&lt;td&gt;Zürich, Switzerland&lt;/td&gt;&lt;td&gt;Dublin, Ireland&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Primary Regulatory Oversight&lt;/td&gt;&lt;td&gt;Swiss Federal Data Protection and Information Commissioner (FDPIC)&lt;/td&gt;&lt;td&gt;Irish Data Protection Commission (DPC)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;IP Address Logging Policy&lt;/td&gt;&lt;td&gt;Absolute zero-log policy for client IP addresses&lt;/td&gt;&lt;td&gt;Temporary and long-term logging of IP allocations&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mandatory Data Retention&lt;/td&gt;&lt;td&gt;Exempt from Swiss SPTA/TCA retention obligations for its resolver service&lt;/td&gt;&lt;td&gt;Legally bound to retain certain user/source data, generally around one year, with special retention orders possible&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GDPR Compliance Pathway&lt;/td&gt;&lt;td&gt;Subject to Swiss law globally; GDPR representative in Hamburg for EEA users&lt;/td&gt;&lt;td&gt;Directly bound under EU GDPR and Irish ePrivacy Regulations&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Quad9 and Swiss Geopolitical Engineering&lt;/h3&gt;
&lt;p&gt;Quad9 is incorporated as a non-profit foundation headquartered in Zürich, Switzerland. This corporate domicile was selected to place the entity under a legal system that strictly enforces individual privacy rights. Quad9 treats user IP addresses as personally identifiable information and aligns its privacy position with Swiss data-protection law and, for EEA users, the European Union’s General Data Protection Regulation (GDPR).
To secure its operations against state-compelled logging, Quad9 relies on Swiss legal protections and has published Swiss regulatory findings about its resolver service:
Exemption from the Telecommunications Act (TCA): The Swiss Federal Office of Communications confirmed that Quad9 is not a telecommunications service under the Swiss TCA, exempting the foundation from “Know Your Customer” (KYC) identity collection mandates.
Exemption from Surveillance Laws (SPTA): The Swiss Post and Telecommunications Surveillance Service ruled that the Federal Act on the Surveillance of Post and Telecommunications does not apply to Quad9. Consequently, Quad9 is not required to collect or retain metadata for law enforcement or intelligence agencies.
Global Swiss Data Protection Coverage: Quad9 says Swiss data protection law applies to users globally, while GDPR also applies to Quad9 users in the EEA.
Because Quad9 does not maintain a subscriber database or log client IP addresses, it possesses no metadata to yield. Any attempt by foreign agencies to compel data handover must go through a Mutual Legal Assistance Treaty (MLAT) evaluated by the Swiss Federal Office of Justice. If a government attempts to use national laws to force data collection, Quad9’s operational charter mandates a complete shutdown of operations in that country, routing local queries to servers in nearby jurisdictions.&lt;/p&gt;
&lt;h3&gt;Vodafone Ireland and Telecommunications Retention Mandates&lt;/h3&gt;
&lt;p&gt;Vodafone Ireland Limited operates as a commercial telecommunications provider under the laws of the Republic of Ireland and the EU. It manages a large network infrastructure under Autonomous System AS15502. Unlike public recursive resolvers, Vodafone is legally bound by the Communications (Retention of Data) (Amendment) Act 2022. This legislation was enacted to align Irish data retention laws with rulings from the Court of Justice of the European Union (CJEU), such as the Graham Dwyer murder conviction appeal, Digital Rights Ireland, and Tele2/Watson.
Under the Communications (Retention of Data) (Amendment) Act 2022, Vodafone Ireland must comply with retention obligations for certain categories of communications data:
Mandatory User and Internet Source Data Retention: Irish law requires service providers to retain certain user data and internet source data, such as data needed to identify the source of an internet communication, generally for around one year, with prescribed periods possible up to two years.
National Security Retention Orders: If the Minister for Justice identifies a serious threat to national security, a relevant judge can issue an order requiring service providers to retain specified Schedule 2 data for 12 months.
Targeted “Quick Freeze” Orders: Irish law enforcement agencies can obtain preservation and production orders to freeze metadata associated with a specific suspect.
When a subscriber queries Vodafone’s default DNS servers (such as the legacy 89.19.64.164 / 89.19.64.36 or dynamically assigned resolvers like 64.43.51.22), the request is sent in cleartext, exposing the queried domain name, timestamp, and subscriber IP address to the access network. Irish retention law should not be described as a clear statutory requirement to retain every DNS query, but plaintext ISP DNS still gives the provider the technical ability to observe those lookups.
Additionally, Vodafone Ireland has faced regulatory scrutiny regarding database management and subscriber preference handling. The Irish Data Protection Commission (DPC) has prosecuted Vodafone Ireland multiple times for unsolicited marketing communications under Regulation 13 of the ePrivacy Regulations, including documented cases in 2011, 2022, and 2023, with other reports noting prior convictions. These compliance failures underscore the privacy risks when sensitive subscriber data is managed by a commercial entity with marketing operations.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Security Profiling, Content Blocking, and State Censorship&lt;/h2&gt;
&lt;p&gt;While recursive DNS resolvers are frequently used to enforce network security, the definition of “safety” differs significantly between a dedicated public security resolver and an ISP resolver subject to state court orders.&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Security &amp;amp; Filtering Dimension&lt;/th&gt;&lt;th&gt;Quad9 Public DNS (9.9.9.9)&lt;/th&gt;&lt;th&gt;Vodafone Ireland Default DNS&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Active Threat Prevention&lt;/td&gt;&lt;td&gt;Automatic blocking of malware, phishing, and C2 servers&lt;/td&gt;&lt;td&gt;None by default; requires a paid Secure Net subscription&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Threat Intelligence Sourcing&lt;/td&gt;&lt;td&gt;Aggregated from 25+ security feeds&lt;/td&gt;&lt;td&gt;None on standard tiers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Copyright Intermediary Blocking&lt;/td&gt;&lt;td&gt;None; rejects copyright-based domain filtering&lt;/td&gt;&lt;td&gt;Compelled DNS-level blocklists for sites like The Pirate Bay&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Dynamic Stream Interception&lt;/td&gt;&lt;td&gt;None&lt;/td&gt;&lt;td&gt;May be required where Vodafone is named in relevant copyright injunctions&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Parental Content Control&lt;/td&gt;&lt;td&gt;Not customizable on the standard profile&lt;/td&gt;&lt;td&gt;Network-level custom filtering available via Secure Net&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Cyber Threat Intelligence vs. Passive Routing&lt;/h3&gt;
&lt;p&gt;The default configuration of Quad9 (9.9.9.9) is designed for active threat mitigation. Quad9 blocks user connections to malicious domains at the DNS layer by aggregating real-time threat intelligence from 25+ independent threat intelligence providers, including the Swiss security center SWITCH. Some independent tests have reported block rates as high as about 97%, though results vary by test set and date. This filtering can prevent endpoints from communicating with malware distributors, phishing landing pages, and Command-and-Control (C2) servers. Quad9 performs this filtering automatically without charging subscription fees or requiring software installation on client devices.
By contrast, Vodafone Ireland’s default DNS servers operate as passive recursors. They do not provide native threat intelligence filtering to protect users from emerging cyber threats. If a Vodafone subscriber clicks an active phishing link while using default DNS, the resolver completes the lookup, leaving endpoint defense entirely to the local machine.&lt;/p&gt;
&lt;h3&gt;ISP-Level Censorship and Legal Intermediary Blocking&lt;/h3&gt;
&lt;p&gt;While Vodafone’s default DNS does not provide dynamic cyber threat filtering, it actively enforces state-mandated content blocking. As a licensed telecom intermediary, Vodafone Ireland is legally compelled to comply with High Court injunctions:
Copyright Infringement Blocks: Following a landmark legal action under “Irish SOPA” legislation, Vodafone was ordered by the Irish High Court to implement DNS-level blocking of indexers like The Pirate Bay and its corresponding mirror sites.
Dynamic Sports Streaming Blocks: Irish ISPs, including Vodafone where named in relevant orders, may be required to comply with copyright injunctions that block access to infringing streaming services.
Quad9 does not block sites for copyright enforcement, trademark disputes, or licensing contentions. Although Quad9 was temporarily subject to an interim injunction in Germany to block domains at the request of Sony Music, the Dresden Higher Regional Court ultimately ruled in Quad9’s favor in December 2023, treating recursive DNS resolvers as neutral intermediaries that benefit from liability privileges under German and EU law. Its mandate remains strictly limited to cyber security threats.&lt;/p&gt;
&lt;h3&gt;Vodafone Secure Net Add-On&lt;/h3&gt;
&lt;p&gt;To provide security filtering similar to public resolvers, Vodafone offers a paid network-level security subscription called Secure Net (Secure Net Home costs €2.99 per month, and Secure Net Mobile costs €1.99 per month). Secure Net is a network-level filtering product operated inside Vodafone’s network. Public Vodafone materials say it can analyse network traffic, block unsafe websites/downloads, and provide parental controls like age-appropriate content filtering, Bedtime schedules, and custom domain blocking, but they do not fully document the technical mechanisms used.
Because Secure Net operates inside Vodafone’s transport loop, VPNs, encrypted proxy services, and some encrypted DNS configurations may bypass parts of its filtering, especially DNS-based filtering, depending on how the product implements detection. It may also be incompatible with browsers that utilize independent data compression.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Cryptographic Protocols and Resolution Capabilities&lt;/h2&gt;
&lt;p&gt;Modern DNS design relies heavily on cryptographic transport security and performance optimization protocols. The architectural division between Quad9 and Vodafone Ireland reveals a stark contrast in their support for secure DNS standards.&lt;/p&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Technical Feature&lt;/th&gt;&lt;th&gt;Quad9 Public DNS&lt;/th&gt;&lt;th&gt;Vodafone Ireland Default DNS&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Standard Unencrypted DNS&lt;/td&gt;&lt;td&gt;Supported on UDP and TCP Port 53&lt;/td&gt;&lt;td&gt;Default resolution protocol&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DNS-over-TLS (DoT)&lt;/td&gt;&lt;td&gt;Supported on TCP Port 853&lt;/td&gt;&lt;td&gt;Not supported on standard ISP servers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DNS-over-HTTPS (DoH)&lt;/td&gt;&lt;td&gt;Supported over HTTPS on port 443&lt;/td&gt;&lt;td&gt;No public support documented for default customer resolvers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DNSSEC Validation&lt;/td&gt;&lt;td&gt;Supported and enforced on secured service profiles&lt;/td&gt;&lt;td&gt;Not verified from public Vodafone documentation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;EDNS Client Subnet (ECS)&lt;/td&gt;&lt;td&gt;Stripped on 9.9.9.9; supported on 9.9.9.11&lt;/td&gt;&lt;td&gt;Not verified from public Vodafone documentation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Extended DNS Errors (EDE)&lt;/td&gt;&lt;td&gt;Support varies by endpoint and implementation&lt;/td&gt;&lt;td&gt;Not verified from public Vodafone documentation&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;The Last-Mile Security Paradox&lt;/h3&gt;
&lt;p&gt;Standard DNS traffic is transmitted in cleartext over UDP or TCP port 53, making it vulnerable to on-path interception, eavesdropping, and tampering by network operators and middleboxes. To prevent this, secure transport protocols have been developed:
DNS-over-TLS (DoT): Wraps DNS queries inside a secure TLS session over dedicated TCP port 853. This encrypts name resolution traffic once configured at the OS, application, or router level.
DNS-over-HTTPS (DoH): Wraps DNS queries inside HTTPS, commonly using HTTP/2 over TLS/TCP 443 or HTTP/3 over QUIC/UDP 443. DoH blends in with standard web traffic, making it difficult for firewalls to block or inspect.
Quad9 supports both DoT and DoH across its main service profiles, providing robust encryption from the user’s stub resolver directly to Quad9’s recursive resolver infrastructure.
Vodafone Ireland’s default name servers do not support incoming DoT or DoH connections. Devices utilizing the default ISP-assigned DNS send queries in cleartext, exposing every domain lookup to local network sniffers, transit providers, and state surveillance systems.
This creates The Last-Mile Security Paradox. If a user configures Quad9’s IP address (9.9.9.9) inside their router but leaves the connection unencrypted over standard port 53, the underlying ISP (Vodafone) can still inspect the DNS query payload. Because the DNS request travels in plaintext across Vodafone’s physical network, the ISP can technically record the queried domains. Therefore, to prevent ISP-path inspection of DNS lookups, users must configure secure DNS protocols like DoH or DoT on their devices to encrypt the traffic before it leaves the local network. This does not make browsing invisible to the ISP; it mainly protects the DNS lookup itself.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-05-27/vodafone-gigabox-dns-conf.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;Only change the settings on &lt;a href=&quot;http://192.168.1.1/internet.html#sub=dns&quot;&gt;http://192.168.1.1/internet.html#sub=dns&lt;/a&gt; from the Gigabox doesn’t save your privacy!&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Infrastructure, Routing Latency, and Network Stability&lt;/h2&gt;
&lt;p&gt;DNS resolution performance directly impacts the responsiveness of web browsing, gaming startup handshakes, and application connection times. For classic TCP-based connections, the mathematical expression for the total time required to establish a secure connection, &lt;em&gt;T&lt;/em&gt;&lt;sub&gt;total&lt;/sub&gt;, is:&lt;/p&gt;
&lt;div&gt;
  T&lt;sub&gt;total&lt;/sub&gt; = T&lt;sub&gt;dns&lt;/sub&gt; + T&lt;sub&gt;tcp&lt;/sub&gt; + T&lt;sub&gt;tls&lt;/sub&gt;
&lt;/div&gt;
&lt;p&gt;Where &lt;em&gt;T&lt;/em&gt;&lt;sub&gt;dns&lt;/sub&gt; represents DNS lookup latency, &lt;em&gt;T&lt;/em&gt;&lt;sub&gt;tcp&lt;/sub&gt; represents the TCP handshake duration, and &lt;em&gt;T&lt;/em&gt;&lt;sub&gt;tls&lt;/sub&gt; represents the cryptographic handshake time (though modern HTTP/3 over QUIC combines the transport and cryptographic handshakes into a single round trip). A slow recursive resolver increases &lt;em&gt;T&lt;/em&gt;&lt;sub&gt;dns&lt;/sub&gt;, delaying the entire network handshake.&lt;/p&gt;
&lt;h3&gt;Anycast Topology and Peering Infrastructure&lt;/h3&gt;
&lt;p&gt;Public DNS resolvers utilize Anycast routing, announcing the same IP address pool from multiple physical data centers globally. BGP routing protocols then direct the client’s DNS query to the topologically nearest active node.&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Performance Variable&lt;/th&gt;&lt;th&gt;Quad9 Anycast System&lt;/th&gt;&lt;th&gt;Vodafone Internal ISP DNS&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Anycast Node Deployment&lt;/td&gt;&lt;td&gt;Globally distributed 230+ Resolver Clusters in over 110 countries&lt;/td&gt;&lt;td&gt;Concentrated within regional ISP network hubs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Peering Presence (Ireland)&lt;/td&gt;&lt;td&gt;Direct INEX peering via PCH AS42&lt;/td&gt;&lt;td&gt;Local gateway peering within AS15502&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Uptime &amp;amp; Redundancy&lt;/td&gt;&lt;td&gt;High; queries automatically route to adjacent nodes if local servers fail&lt;/td&gt;&lt;td&gt;Dependent on the availability of local ISP recursors&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Average Global Latency&lt;/td&gt;&lt;td&gt;~21 ms (DNSPerf global average)&lt;/td&gt;&lt;td&gt;Highly dependent on the local subscriber loop&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;EDNS Client Subnet (ECS)&lt;/td&gt;&lt;td&gt;Disabled on standard tier; may cause sub-optimal CDN routing&lt;/td&gt;&lt;td&gt;Not verified from public Vodafone documentation&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Quad9 partners with Packet Clearing House (PCH), which maintains DNS nodes across many Internet Exchange Points globally. PCH’s AS42 joined the Internet Neutral Exchange Association (INEX) in 2009. It peers with a 10 Gbps port at Equinix DB2 Kilcarbery, Dublin, assigning IP addresses 185.6.36.60 and 2001:7f8:18::60. This can allow Irish networks with favourable routing or peering to reach Quad9 with very low latency.
Vodafone Ireland (AS15502) handles massive IP space. Because its default recursive servers sit directly inside the subscriber’s broadband access path, they can resolve cached records with minimal latency. However, Vodafone’s DNS relies on localized routing and lacks the globally distributed redundancy of a multi-node anycast network. If a local recursor fails, standard fallback relies on the secondary server, which may still be affected by local network congestion.
Standard Quad9 9.9.9.9 strips ECS to protect privacy, which can cause some CDNs to make less optimal routing decisions. Vodafone IE’s DNS may provide strong local routing for content providers, but its ECS behaviour is not verified from public Vodafone documentation.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;How to Actually Use Quad9 with DoT or DoH&lt;/h2&gt;
&lt;p&gt;Quad9’s official documentation is available at &lt;a href=&quot;https://docs.quad9.net/&quot;&gt;docs.quad9.net&lt;/a&gt;. Their recommended secure service is &lt;code&gt;9.9.9.9&lt;/code&gt;, which provides DNSSEC validation and threat blocking. But just like I mentioned above, simply using Quad9 over plain port 53 on your router will not hide DNS lookups from your ISP. To protect the DNS query itself, use DoH or DoT.&lt;/p&gt;
&lt;p&gt;For encrypted DNS, the important hostname is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;dns.quad9.net&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your device does not support DoH or DoT natively, such as some ISP routers, using Quad9’s secure resolver addresses is still useful for DNSSEC validation and threat blocking, but it will not encrypt the DNS traffic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;IPv4: 9.9.9.9, 149.112.112.112&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;IPv6: 2620:fe::fe, 2620:fe::9&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For most people, &lt;strong&gt;DoH is the safer default choice on laptops and phones&lt;/strong&gt; because it is less likely to be blocked on guest Wi-Fi. &lt;strong&gt;DoT is great on networks you control&lt;/strong&gt;, such as your home Wi-Fi or a router you manage.&lt;/p&gt;
&lt;h3&gt;Android 9 and later&lt;/h3&gt;
&lt;p&gt;Android has built-in DNS-over-TLS support through &lt;strong&gt;Private DNS&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;Settings&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;Private DNS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;Private DNS provider hostname&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enter &lt;code&gt;dns.quad9.net&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tap &lt;code&gt;Save&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Quad9 notes that Android’s Private DNS will not be used while a VPN is active, and it may conflict with the Quad9 Connect app if that app is installed and enabled.&lt;/p&gt;
&lt;h3&gt;iPhone, iPad, and macOS&lt;/h3&gt;
&lt;p&gt;For iOS 14+ and macOS Big Sur+, Quad9 provides downloadable encrypted DNS profiles. Their docs recommend &lt;strong&gt;DNS-over-HTTPS for most users&lt;/strong&gt;, especially on guest Wi-Fi or networks you do not control. DNS-over-TLS is better suited to networks where you know port &lt;code&gt;853&lt;/code&gt; is allowed.&lt;/p&gt;
&lt;p&gt;The basic process is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open Quad9’s official setup guide in &lt;strong&gt;Safari&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;9.9.9.9&lt;/code&gt; &lt;strong&gt;HTTPS&lt;/strong&gt; profile for DoH, or the &lt;code&gt;9.9.9.9&lt;/code&gt; &lt;strong&gt;TLS&lt;/strong&gt; profile for DoT.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;Settings&lt;/code&gt; / &lt;code&gt;System Settings&lt;/code&gt; and install the downloaded profile.&lt;/li&gt;
&lt;li&gt;Remember that Apple’s encrypted DNS profiles expire; Quad9’s current documentation says the profiles expire on &lt;strong&gt;19 January 2027&lt;/strong&gt;, so future-you may need to install a fresh one.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Quad9 also notes that iCloud Private Relay, many VPN clients, and tools like Little Snitch may ignore or override the DNS profile.&lt;/p&gt;
&lt;h3&gt;Windows 11&lt;/h3&gt;
&lt;p&gt;Windows 11 can use DNS-over-HTTPS directly from network settings:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;Network and Internet Settings&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select your active &lt;code&gt;Wi-Fi&lt;/code&gt; or &lt;code&gt;Ethernet&lt;/code&gt; connection.&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Edit&lt;/code&gt; next to &lt;code&gt;DNS server assignment&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Change it from &lt;code&gt;Automatic (DHCP)&lt;/code&gt; to &lt;code&gt;Manual&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enable &lt;code&gt;IPv4&lt;/code&gt; and enter:
&lt;ul&gt;
&lt;li&gt;Preferred DNS: &lt;code&gt;9.9.9.9&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Alternate DNS: &lt;code&gt;149.112.112.112&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;DNS over HTTPS&lt;/code&gt; to &lt;code&gt;On (automatic template)&lt;/code&gt; for both.&lt;/li&gt;
&lt;li&gt;If you use IPv6, also enter:
&lt;ul&gt;
&lt;li&gt;Preferred DNS: &lt;code&gt;2620:fe::fe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Alternate DNS: &lt;code&gt;2620:fe::9&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save the settings.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Quad9’s Windows guide also warns that VPNs usually ignore system DNS settings. If you use a VPN, configure Quad9 inside the VPN client’s &lt;code&gt;Custom DNS&lt;/code&gt; settings instead.&lt;/p&gt;
&lt;h3&gt;Check if it worked&lt;/h3&gt;
&lt;p&gt;After setting it up, visit &lt;a href=&quot;https://on.quad9.net/&quot;&gt;on.quad9.net&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Quad9 uses this page to confirm whether your device is using Quad9. On Windows, Quad9’s documentation also suggests checking the protocol with PowerShell:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Resolve-DnsName&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span&gt;Type txt proto.on.quad9.net.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the result says &lt;code&gt;doh&lt;/code&gt;, congratulations: your DNS is wearing a tiny encrypted trench coat.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;If you’re using a proxy, make sure to configure Quad9 inside the proxy client’s &lt;code&gt;Custom DNS&lt;/code&gt; settings instead of relying solely on system DNS settings. Depending on how your proxy handles DNS routing (particularly on macOS or with certain split-tunnel configurations), you may see DNS traffic routed through both your proxy tunnel and local resolvers, which can look like a DNS leak to privacy checkers or connection logs.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-05-27/VPN-conflict.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;This commonly happens on desktop systems like macOS if the proxy client does not capture all resolver scopes. On mobile operating systems like iOS and Android, system-level encrypted DNS (such as Android’s Private DNS or Apple DNS profiles) is generally bypassed or suppressed in favour of the active VPN tunnel.&lt;/p&gt;
&lt;p&gt;And if it does not work the first time, don’t panic. DNS is just the internet’s phonebook, and like every phonebook, sometimes it has been left under a router, behind a sofa, guarded by a very confused cat.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;One last thing&lt;/h2&gt;
&lt;p&gt;Encrypted DNS is not the same thing as full browsing anonymity. Even with DoH or DoT, your ISP can still see destination IP addresses, connection timing, traffic volume, and sometimes the hostname exposed through TLS metadata such as SNI. &lt;a href=&quot;https://blog.cloudflare.com/announcing-encrypted-client-hello/&quot;&gt;Encrypted Client Hello (ECH)&lt;/a&gt; is designed to hide more of that TLS handshake metadata, but it only works when both your browser and the website/CDN support it. You can check whether your browser is using ECH with &lt;a href=&quot;https://crypto.cloudflare.com/cdn-cgi/trace&quot;&gt;Cloudflare’s trace page&lt;/a&gt; or &lt;a href=&quot;https://test.defo.ie/&quot;&gt;test.defo.ie&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But this is for another story.&lt;/p&gt;</content:encoded></item><item><title>Everyone should start using a VPN or Proxy</title><link>https://michifumi.de/blog/2026-04-25-everyone-should-start-using-a-vpn-or-proxy/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-04-25-everyone-should-start-using-a-vpn-or-proxy/</guid><description>A VPN secures your traffic even on untrusted networks, while an obfuscated proxy bypasses strict VPN blocks, giving you unrestricted access to the open web.</description><pubDate>Sat, 25 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A VPN secures your traffic even on untrusted networks, while an obfuscated proxy bypasses strict VPN blocks, giving you unrestricted access to the open web.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;If you have watched &lt;a href=&quot;https://www.youtube.com/watch?v=46hy3r_1VqY&quot;&gt;this video&lt;/a&gt;, you might start worrying about how to stop your ISP from finding out more about you than your mum does. Even with ECH to secure the SNI (you can test it on &lt;a href=&quot;https://www.cloudflare.com/ssl/encrypted-sni/&quot;&gt;this Cloudflare website&lt;/a&gt;), you still can’t prevent ISPs or Wi-Fi owners from recording, or even hijacking your internet traffic. Not to mention that some internet services are still only supporting HTTP traffic until 2026 (This is pure criminality).&lt;/p&gt;
&lt;p&gt;So, what’s the big deal? Someone might ask. Well, it’s not a big deal as long as you don’t mind your personal interests being “accidentally” sold to ads companies, or your personal information being stolen by hackers harvesting credentials from individuals who connect to fake Wi-Fi networks, just like what happened in &lt;a href=&quot;https://www.bitdefender.com/en-gb/blog/hotforsecurity/australian-federal-police-arrest-suspect-for-wifi-credential-theft-on-flights&quot;&gt;Australian&lt;/a&gt; 2 years ago. I could talk for hours about the dangers of leaking your internet traffic to a third party, which could end up in the hands of criminals. However, that is not the purpose of this blog. Here, I want to introduce how to protect yourself from internet tracking using a VPN, and a self-hosted proxy with obfuscation capabilities.&lt;/p&gt;
&lt;p&gt;For most non-tech users, the easiest way to use a VPN is to purchase the service from a commercial company. If you are not familiar with VPN, I strongly recommend reading &lt;a href=&quot;https://windscribe.com/blog/a-modern-vpn-service-is-not-what-you-think-it-is/&quot;&gt;this article&lt;/a&gt;, which explains it from scratch and clarifies a lot of misunderstandings such as using a VPN alone does &lt;strong&gt;not&lt;/strong&gt; provide anonymity, and it’s also fun to read.&lt;/p&gt;
&lt;p&gt;However, using a VPN means that you are placing your trust in the hands of VPN companies rather than ISPs, which could also be dangerous if the company does not have a good reputation or reliable technology. So, choosing a VPN provider is the only thing the average user needs to consider.&lt;/p&gt;
&lt;p&gt;In today’s increasingly mature VPN market, almost all major providers use similar technologies—such as WireGuard or OpenVPN. These are tried-and-tested, reliable encryption technologies that are perfectly adequate for general use. When there is little difference in technical ability, from my personal experience, the most important thing is to never use a free one, whether they claim to be supporting digital human rights or have a grand vision for the future of the company. Nerver, ever use a VPN for free, otherwise, your worth will be measured in a different way. Just like Mullvad said in its &lt;a href=&quot;https://mullvad.net/en/pricing&quot;&gt;official FAQ&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Free” services nearly always come at some cost, whether that be the time you spend watching an intro ad, the collection of your data, or by limiting the functionality of the service.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Another key consideration is reputation and whether the company has withstood scrutiny by the judicial system. A good reputation means that the company has experience in dealing with hackers who try to exploit vulnerabilities in its systems to access customers’ personal data, and undergoes regular audits by third party organisations. Having withstood scrutiny by the judicial authorities is another excellent “badge of honour”, signifying that even state-level measures were unable to extract any meaningful user information. If you search for related news using a search engine or AI, you will find that there are not many companies with this kind of “badge of honour”.&lt;/p&gt;
&lt;p&gt;Up to this point, the VPN has appeared to be unrivalled. But hang on a sec, the VPN also has its limitations, particularly when it comes to targeted measures, for instance, the firewall. Because VPN traffic is so easily identifiable, a network administrator can easily block any connection encrypted via a VPN if they so wish. The Chinese, Iranians and Russians know this all too well. However, apart from them, even those living in the free Western world face the same predicament. If you have used a public Wi-Fi network in a place like a college or coffee shop, you may have noticed that some VPN services are unable to connect in such environments. That’s probably because the Wi-Fi provider is blocking specific traffic protocols or common VPN ports. This situation is usually even more frustrating, not only does it mean that your internet service provider can monitor everything you do, but they can also block access to websites, IP addresses or even network ports that they don’t want you to visit. When the VPN can do nothing about this and strong encryption is not your first priority, the proxy with obfuscation capabilities comes into play.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/shadowsocks/shadowsocks-rust&quot;&gt;Shadowsocks-rust&lt;/a&gt; and &lt;a href=&quot;https://github.com/XTLS/Xray-core&quot;&gt;Project X&lt;/a&gt; were created for this very purpose, both are highly reputable, open-source, and considered “gold standard” tools within the network proxy and anti-censorship communities. I have used them for many years and they have never failed me. That’s why I wrote a &lt;a href=&quot;https://github.com/Shawshank01/proxy_sh&quot;&gt;shell script&lt;/a&gt; for them, to simplify the process of building a proxy server on a VPS deployed by Docker.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-04-25/proxy-script-cli.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
Script Menu&lt;/p&gt;
&lt;h2&gt;What are the differences, and which one should you choose?&lt;/h2&gt;
&lt;h3&gt;Shadowsocks&lt;/h3&gt;
&lt;p&gt;It’s a lightweight, rock-solid, and fast proxy for general privacy or bypassing simple blocks without the overhead of complex protocols. I specifically chose SS-2022 as the protocol to enhance modern security, and it makes traffic appear as a random, featureless stream of unknown TCP/UDP bytes to an ISP or network administrator. Most ISPs and administrators wouldn’t care about this unknown traffic, and you can bypass their VPN restrictions. If you set the proxy port to a commonly used port, such as 80 or 443, you can further increase your chances of bypassing the restrictions.&lt;/p&gt;
&lt;h3&gt;Xray&lt;/h3&gt;
&lt;p&gt;If you live in China, Iran or Russia, or if your ISP restricts unknown traffic and only allows HTTPS/TLS connections (as is common when using Wi-Fi provided by a library or college), then you will need to choose Xray, which is a slightly more complicated but more powerful proxy tool. Don’t worry, I’ve made it as easy as possible to reduce the number of steps in the setup process in my script.&lt;/p&gt;
&lt;p&gt;Xray provides several ways to bypass detection and restrictions using obfuscation. I specifically opted for the VLESS-XHTTP-REALITY method. The best part about it is that the only thing you need to prepare is a VPS or a remote Linux server that runs 24/7 with no network restrictions. No domain or certificates are needed, and you can “steal” another website’s domain to make it look like your traffic is connecting to that domain via HTTPS! In reality, however, your traffic is encrypted from your local machine to the proxy server and then on to the target website or internet service that you want to visit. The only tricky part is that you have to find a website that can be visited directly from the local network without a proxy. That website also needs to meet some requirements. I have listed the &lt;a href=&quot;https://github.com/Shawshank01/proxy_sh#configuration-details&quot;&gt;details in my GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;If you want to pursue ultimate proxy camouflage, there is a technique revered in the Xray community as the “perfect answer”, borrowing a neighbor’s domain. Instead of using famous public domain names, you can follow below methods to tailor a domain specifically for your proxy server.&lt;/p&gt;
&lt;/div&gt;
&lt;h4&gt;SHODAN&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open &lt;a href=&quot;https://www.shodan.io/&quot;&gt;SHODAN&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Replace the IP address below with your own and search.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;net:&quot;your-ip-address/22&quot; port:443 ssl.version:TLSv1.3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select a normal website and run test scripts on your proxy server directly.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;bgp.tools&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Open the website: &lt;a href=&quot;https://bgp.tools&quot;&gt;bgp.tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Enter your VPS’s public IP in the search box and press Enter.&lt;/li&gt;
&lt;li&gt;Click the DNS tab, then check “Remove auto generated reverse DNS entries”.&lt;/li&gt;
&lt;li&gt;You will now see a large list of other domain names located in the same data center and subnet as your VPS.&lt;/li&gt;
&lt;li&gt;Select a normal website and run test scripts on your proxy server directly.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;HE&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Enter your VPS IP address at &lt;a href=&quot;https://bgp.he.net&quot;&gt;HE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Click on the IP address that looks like &lt;code&gt;x.x.x.0/22&lt;/code&gt; under &lt;code&gt;Announcement&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Click the DNS tab.&lt;/li&gt;
&lt;li&gt;Select a normal website and run test scripts on your proxy server directly.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Why bother?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Because these websites reside in the same data centre, or even on the same rack cabling, as your VPS. When Xray’s core “borrows” its TLS certificate and handshake characteristics, latency is under 1 ms. Furthermore, from the perspective of the network administrator, the physical network routing of your VPS traffic matches that of the legitimate neighbor site, maximising camouflage.&lt;/p&gt;
&lt;p&gt;Before entering the domain name into your &lt;em&gt;proxy.sh&lt;/em&gt; script, make sure to avoid redirects: If you enter &lt;code&gt;example.com&lt;/code&gt; and it automatically redirects to &lt;code&gt;www.example.com&lt;/code&gt; or &lt;code&gt;example.com/en/&lt;/code&gt;, make sure to put the final redirected full domain into your script.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Remember that the risks of hosting your own proxy server on a cloud server owned by a business are the same as using a VPN from a commercial company, you are putting your trust in the VPS provider instead of your local ISP. This means that you have to trust the VPS provider not to sell your data to others, either intentionally or unintentionally.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://windscribe.com/blog/a-modern-vpn-service-is-not-what-you-think-it-is/&quot;&gt;A modern VPN service is not what you think it is&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ssd.eff.org/module/choosing-vpn-thats-right-you&quot;&gt;Choosing the VPN That’s Right for You&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>The weird (?) dream</title><link>https://michifumi.de/blog/2026-04-17-the-weird-dream/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-04-17-the-weird-dream/</guid><description>Or should I be happy about it?</description><pubDate>Fri, 17 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Or should I be happy about it?&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-04-17/Gemini_Generated_Image_of_My_English_Dream.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;This image was generated by AI and is not intended to imply anything&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I had a really weird dream yesterday. I was in a random supermarket and when I went to the self-checkout, a guy helped me scan all my items (which is highly unlikely in real life). However, he miscounted them and the machine showed an absurdly high charge. I pointed out the mistake and yelled at him (something I would never do in real life), but he didn’t seem to care at all.&lt;/p&gt;
&lt;p&gt;I had no choice but to find the manager and explain everything that had happened. The manager was reasonable and allowed me (so weird!) to correct the count. After that, I happily went back to pick out some new items with the guy who had miscounted them (Yes, that’s how dreams work 😂 — they make no sense at all).&lt;/p&gt;
&lt;p&gt;But the weirdest part isn’t any of that. When I woke up, I realised that everything that had happened in my dream had been in English. For context, my native language is Mandarin. Until last night, I had only ever dreamt in my native language.&lt;/p&gt;
&lt;p&gt;Pretty insane, isn’t it?&lt;/p&gt;
&lt;p&gt;This is my third year in Ireland. I suppose that’s just the power of the environment.&lt;/p&gt;</content:encoded></item><item><title>yt-dlp: Video Downloader with Open-Source GUI</title><link>https://michifumi.de/blog/2026-03-17-yt-dlp-the-ultimate-command-line-video-downloader-with-open-source-gui/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-03-17-yt-dlp-the-ultimate-command-line-video-downloader-with-open-source-gui/</guid><description>How to use yt-dlp to download videos from YouTube and thousands of other sites, with a simple and lightweight graphical user interface.</description><pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;How to use yt-dlp to download videos from YouTube and thousands of other sites, with a simple and lightweight graphical user interface.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;In &lt;a href=&quot;/blog/2026-02-19-ffmpeg-the-ultimate-cross-platform-video-tool/&quot;&gt;this previous blog&lt;/a&gt;, I introduced you to FFmpeg, and now I’m going to introduce you to another excellent open-source tool: &lt;a href=&quot;https://github.com/yt-dlp/yt-dlp&quot;&gt;yt-dlp&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;yt-dlp is a feature-rich command-line audio/video downloader with support for thousands of sites.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You can use it for all sorts of things, but the feature you’ll probably use most often is downloading videos or audio from YouTube to your archive, given the crazy censorship on YouTube these days. If you want to save a video that disappears within a few hours of being uploaded, give yt-dlp a go!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;First, install it using Homebrew:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; yt-dlp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Common Commands&lt;/h2&gt;
&lt;p&gt;Here are some commands I usually use:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Download the best quality available (Standard):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;yt-dlp&lt;/span&gt;&lt;span&gt; --cookies-from-browser&lt;/span&gt;&lt;span&gt; brave&lt;/span&gt;&lt;span&gt; &apos;URL&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;List all available formats for a video:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;yt-dlp&lt;/span&gt;&lt;span&gt; -F&lt;/span&gt;&lt;span&gt; --cookies-from-browser&lt;/span&gt;&lt;span&gt; brave&lt;/span&gt;&lt;span&gt; &apos;URL&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Download a specific format (e.g., format 299+140):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;yt-dlp&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; 299+140&lt;/span&gt;&lt;span&gt; --cookies-from-browser&lt;/span&gt;&lt;span&gt; brave&lt;/span&gt;&lt;span&gt; &apos;URL&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Download all available subtitles without downloading the video:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;yt-dlp&lt;/span&gt;&lt;span&gt; --write-subs&lt;/span&gt;&lt;span&gt; --write-auto-subs&lt;/span&gt;&lt;span&gt; --sub-langs&lt;/span&gt;&lt;span&gt; all&lt;/span&gt;&lt;span&gt; --skip-download&lt;/span&gt;&lt;span&gt; &apos;URL&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Download and merge into an MP4 container (FFmpeg is required):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;yt-dlp&lt;/span&gt;&lt;span&gt; --merge-output-format&lt;/span&gt;&lt;span&gt; mp4&lt;/span&gt;&lt;span&gt; --cookies-from-browser&lt;/span&gt;&lt;span&gt; brave&lt;/span&gt;&lt;span&gt; &apos;URL&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;To download videos from YouTube, it is better to use your browser’s cookies (e.g., Brave, Firefox, or Chrome) to increase the success rate; using &lt;code&gt;--cookies-from-browser brave&lt;/code&gt; allows &lt;code&gt;yt-dlp&lt;/code&gt; to bypass bot detection and access age-restricted content by using your browser’s session. However, this is usually not necessary for other platforms like X or Vimeo.&lt;/li&gt;
&lt;li&gt;If you want to use Safari’s cookies on macOS, you must grant &lt;strong&gt;Full Disk Access&lt;/strong&gt; to your terminal emulator (e.g., Terminal or iTerm) or GUI app in &lt;strong&gt;System Settings &amp;gt; Privacy &amp;amp; Security &amp;gt; Full Disk Access&lt;/strong&gt;, as Safari’s cookie database is protected by macOS security policies.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;However, as I used it more and more, I increasingly felt that using the command line wasn’t a convenient option. So, since laziness is the mother of invention, I decided to write my own Electron-based GUI to free myself from the command line for some of my simple, routine tasks.&lt;/p&gt;
&lt;p&gt;And this is it, the &lt;a href=&quot;https://github.com/Shawshank01/yt-downloader-electron&quot;&gt;YT-DLP Downloader&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is a user-friendly desktop application for downloading videos from YouTube and other supported platforms. It uses &lt;code&gt;yt-dlp&lt;/code&gt; for download, and &lt;code&gt;FFmpeg&lt;/code&gt; to change the format or add subtitles. But this app itself doesn’t bundle yt-dlp and FFmpeg, since they rely heavily on aggressive upgrades. I don’t want to upgrade my app every time they release an upgrade. This means that the main advantage of this app is that it only calls the &lt;code&gt;yt-dlp&lt;/code&gt; or &lt;code&gt;FFmpeg&lt;/code&gt; that are already installed on your system when needed. Every time you upgrade your dependencies, it will use the latest version seamlessly. The downside is that this app is useless without dependencies. But I’m sure that won’t be a problem if you’re a fan of my blog ;-)&lt;/p&gt;
&lt;p&gt;This is what the app looks like:&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-03-23/yt-dlp-downloader.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;The most basic way to use it is to enter a website address (I’ll use &lt;a href=&quot;https://www.youtube.com/watch?v=dQw4w9WgXcQ&quot;&gt;https://www.youtube.com/watch?v=dQw4w9WgXcQ&lt;/a&gt; as an example), select which browser to retrieve cookies from (or skip this step), click the “Choose” button to select a download folder, and finally click the blue “Run” button and wait for the process to complete. It’s that simple!&lt;/p&gt;
&lt;h2&gt;Advanced Actions&lt;/h2&gt;
&lt;p&gt;Here are some slightly more advanced Actions:&lt;/p&gt;
&lt;h3&gt;List Formats&lt;/h3&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-03-23/yt-dlp-downloader-list-formats.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;It will show all the audio and video formats that you can download from the source. After that, you can select the “Download (Custom Format)” Action to specifically combine the audio and video tracks.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-03-23/yt-dlp-downloader-choose-formats.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;Download Subtitles&lt;/h3&gt;
&lt;p&gt;For the Download Subtitles, it allows you to download not only subtitles uploaded by creators themselves, but also subtitles automatically generated by YouTube. It also tells you whether the downloaded subtitles were uploaded by the author or generated automatically. Pretty smart, isn’t it?&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-03-23/yt-dlp-downloader-download-subtitles.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3&gt;Download &amp;amp; Re-encode as high quality MP4 (H.264/AAC)&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Download &amp;amp; Re-encode as high quality MP4 (H.264/AAC)&lt;/strong&gt; Action is for compatibility. Today, a large number of videos on YouTube use the av01 and opus codec, but many people’s older devices or software are unable to decode these fancy new formats. So if you want to share downloaded videos with friends and ensure they can watch them properly, you might find this feature useful. However, for most people, you can simply ignore it.&lt;/p&gt;
&lt;h3&gt;Download &amp;amp; Add Hardsub&lt;/h3&gt;
&lt;p&gt;At last, the &lt;strong&gt;Download &amp;amp; Add Hardsub&lt;/strong&gt; Action is the very reason I developed this GUI. It allows you to download videos and subtitles with a single click, converting the video into an H.264 or HEVC file with hardcoded subtitles. This means the subtitles are burned directly into the video frames, rather than requiring manual loading like external subtitle files, ensuring they will always appear in the video.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-03-23/yt-dlp-downloader-add-hardsub.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;By default, it uses &lt;strong&gt;VideoToolbox&lt;/strong&gt; for hardware acceleration on macOS.&lt;/p&gt;
&lt;p&gt;The “Check for Dependencies” button in the lower-right corner automatically checks whether &lt;code&gt;Homebrew&lt;/code&gt;, &lt;code&gt;yt-dlp&lt;/code&gt;, and &lt;code&gt;FFmpeg&lt;/code&gt; are installed on your macOS. If they are not installed, it also provides a one-click installation feature. If this is your first time using this GUI, or if you’re using it on a brand-new system, you’ll want to click on it to test it out before you start.&lt;/p&gt;
&lt;p&gt;The “Check for Updates” button in the bottom-left corner lets you see if I’ve released a new version on GitHub (Yes, I still occasionally optimise or add new features so I can enjoy life with even less effort.) If there is a new version, you’ll be asked if you want to open the latest releases page in your default browser (of course, you can also click &lt;a href=&quot;https://github.com/Shawshank01/yt-downloader-electron/releases&quot;&gt;this link&lt;/a&gt; to download it).&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;BTW, this blog is not affiliated with Apple. It’s just that I happen to like using their products.&lt;/p&gt;</content:encoded></item><item><title>The Rise, Fall, and Resurrection of JPEG XL</title><link>https://michifumi.de/blog/2026-03-05-the-rise-fall-and-resurrection-of-jpeg-xl/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-03-05-the-rise-fall-and-resurrection-of-jpeg-xl/</guid><description>The story of JPEG XL&apos;s rollercoaster journey, and how you can embrace it on macOS.</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;The story of JPEG XL&apos;s rollercoaster journey, and how you can embrace it on macOS.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;If you’ve read my &lt;a href=&quot;/blog/2026-02-26-blucher-procedural-justice-or-bureaucracy/&quot;&gt;previous blog post&lt;/a&gt;, you may have noticed something odd, namely, broken images. That’s because I used the &lt;code&gt;.jxl&lt;/code&gt; format, which most browsers don’t support out of the box.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;#how-to-enable-jpeg-xl-in-your-browser&quot;&gt;Go directly to the section on how to display JXL images in your browser&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My interest in JPEG XL began when I wrote a research paper on image compression during my graduate studies. While I was impressed by JPEG XL’s capabilities, I was astonished by how few mainstream browsers supported it at the time. Today, I finally have the chance to share the dramatic saga of JPEG XL.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Act I: The Contender Arrives&lt;/h2&gt;
&lt;p&gt;The year is 2020. The world of image formats is a battlefield. WebP, Google’s scrappy challenger, has been fighting for years to dethrone the ancient JPEG. PNG holds the lossless throne. And then, from the halls of the Joint Photographic Experts Group and the ashes of two competing proposals (Google’s PIK and Cloudinary’s FUIF, an evolution of FLIF), a new format is born.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JPEG XL.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It was, by any technical measure, a marvel. It offered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Superior compression to JPEG, WebP, and AVIF in some scenarios&lt;/li&gt;
&lt;li&gt;Lossless &lt;em&gt;and&lt;/em&gt; lossy modes&lt;/li&gt;
&lt;li&gt;HDR and wide color gamut support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lossless JPEG recompression&lt;/strong&gt; — the killer feature that could shrink every JPEG on the internet by ~20% &lt;em&gt;without touching a single pixel&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Progressive decoding, so images could load beautifully even over slow connections&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The web community held its breath. Could this be &lt;em&gt;the one&lt;/em&gt;?&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Act II: The Google Guillotine&lt;/h2&gt;
&lt;p&gt;Chrome shipped JPEG XL support behind a flag in 2021. Developers experimented. Photographers rejoiced. Advocates wrote breathless blog posts. The momentum felt real.&lt;/p&gt;
&lt;p&gt;Then, in October 2022, the axe fell.&lt;/p&gt;
&lt;p&gt;Google filed a Chromium bug — &lt;strong&gt;not to fix JPEG XL, but to remove it&lt;/strong&gt;. The reasoning offered was thin and, to many, deeply suspicious: there wasn’t “enough interest” from the ecosystem, and AVIF (a format Google had heavily invested in) supposedly covered the use cases.&lt;/p&gt;
&lt;p&gt;The backlash was &lt;em&gt;immediate and volcanic&lt;/em&gt;. The bug tracker became a war zone. Hundreds of developers, photographers, and engineers flooded the comments with technical arguments, pleas, and barely-concealed fury. The &lt;a href=&quot;https://issues.chromium.org/issues/40168998&quot;&gt;thread&lt;/a&gt; grew to become one of the most commented issues in Chromium’s history.&lt;/p&gt;
&lt;p&gt;Critics smelled a rat. Google had a vested interest in AVIF — it was derived from the AV1 video codec, which Google co-developed. JPEG XL threatened it directly. Was this a technical decision, or a &lt;em&gt;corporate&lt;/em&gt; one?&lt;/p&gt;
&lt;p&gt;Google didn’t blink. In January 2023, Chrome’s support was &lt;strong&gt;removed&lt;/strong&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Act III: The Wilderness&lt;/h2&gt;
&lt;p&gt;JPEG XL was now in the desert. Chrome — which commands over 60% of the browser market — had slammed the door. Without Chrome, web developers couldn’t use the format. Without web developers, there was no ecosystem. Without an ecosystem, what was the point?&lt;/p&gt;
&lt;p&gt;The format seemed doomed to join the graveyard of “technically superior formats that lost anyway” — a cemetery already crowded with headstones.&lt;/p&gt;
&lt;p&gt;But something unusual happened in that wilderness.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The community refused to bury it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Chromium bug thread kept growing. Open-source advocates kept maintaining encoders like &lt;code&gt;cjxl&lt;/code&gt;. The format’s spec was finalized. And critically — other browsers were &lt;em&gt;watching&lt;/em&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Act IV: The Faithful Hold the Line&lt;/h2&gt;
&lt;p&gt;Safari had quietly shipped JPEG XL support in 2023 and &lt;strong&gt;kept it&lt;/strong&gt;. While Apple is also a member of the Alliance for Open Media (supporting AVIF), they chose to support both formats. This gave JPEG XL a crucial lifeline.&lt;/p&gt;
&lt;p&gt;Firefox sat on the fence. It had supported JPEG XL in its Nightly builds since version 90.0a1 in May 2021, but locked it behind the &lt;code&gt;image.jxl.enabled&lt;/code&gt; flag in &lt;code&gt;about:config&lt;/code&gt;. For years, they cited Chrome’s decision as a reason to wait, keeping the door open but the feature firmly opted-in only.&lt;/p&gt;
&lt;p&gt;Meanwhile, the JPEG XL advocates did something remarkable: they built the case &lt;em&gt;methodically and publicly&lt;/em&gt;. Benchmarks were published. Real-world use cases were documented. The lossless JPEG recompression argument — imagine shrinking the internet’s collective JPEG archive by 20% &lt;em&gt;for free&lt;/em&gt; — gained new attention as bandwidth costs climbed.&lt;/p&gt;
&lt;p&gt;The format found passionate homes in photography communities, archivists, and HDR content creators who needed what nothing else could offer.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Act V: The Tide Turns&lt;/h2&gt;
&lt;p&gt;The dam began to crack. Operating system support broadened when Microsoft added JPEG XL decoding to Windows 11 via an official extension, bringing support to the Photos app and File Explorer.&lt;/p&gt;
&lt;p&gt;Then came the moment nobody had quite predicted: the CDN and infrastructure giants started caring. Cloudflare, Cloudinary, and others began supporting JPEG XL delivery, quietly building the ecosystem that Google said didn’t exist.&lt;/p&gt;
&lt;p&gt;As of March 2026, Firefox is on the precipice. While stable, beta, and developer editions still don’t support it by default, Mozilla is actively integrating a new Rust-based decoder &lt;code&gt;jxl-rs&lt;/code&gt;. The implementation is pending, but the groundwork for full support in a future stable release is being laid.&lt;/p&gt;
&lt;p&gt;And then — slowly, without fanfare — Google itself began to reconsider. With the web ecosystem shifting, with Safari aligned and Firefox laying the foundation, with tooling maturing, the pressure on Chrome became harder to ignore.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Epilogue: An Unfinished Story&lt;/h2&gt;
&lt;p&gt;As of today, JPEG XL is not yet universal. Default support in Chrome remains the missing piece that would truly complete the story. But the format is very much alive, supported natively in Apple’s ecosystem (macOS, iOS, Safari), available behind experimental flags in Firefox and Chrome, embraced by major CDNs, and living in countless tools.&lt;/p&gt;
&lt;p&gt;The story of JPEG XL is ultimately a story about &lt;strong&gt;who controls the web’s infrastructure&lt;/strong&gt; and whether technical merit alone can survive corporate headwinds. It’s a story about a community that refused to accept a burial decree, that kept writing code and making arguments until the ground shifted.&lt;/p&gt;
&lt;p&gt;JPEG has survived for over 30 years. Its successor, it seems, may be just stubborn enough to do the same.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The arc is long. But it bends toward better compression.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;If you’ve read this far, I’m guessing you’re quite interested in embracing JPEG XL. In the following section, you’ll learn how to actually use it on macOS.&lt;/p&gt;
&lt;p&gt;First, let’s install the JPEG XL command-line tools (&lt;code&gt;cjxl&lt;/code&gt; and &lt;code&gt;djxl&lt;/code&gt;) on your Mac using Homebrew:&lt;/p&gt;
&lt;h2&gt;Install Homebrew&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/bin/bash&lt;/span&gt;&lt;span&gt; -c&lt;/span&gt;&lt;span&gt; &quot;$(&lt;/span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -fsSL&lt;/span&gt;&lt;span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Install JPEG XL tools&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; jpeg-xl&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(Or via MacPorts: &lt;code&gt;sudo port install libjxl&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Basic Conversions in Terminal&lt;/h2&gt;
&lt;p&gt;Once installed, you can use the command-line tools &lt;code&gt;cjxl&lt;/code&gt; (to encode/compress) and &lt;code&gt;djxl&lt;/code&gt; (to decode/decompress).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Convert a JPEG to a JXL:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cjxl&lt;/span&gt;&lt;span&gt; input.jpeg&lt;/span&gt;&lt;span&gt; output.jxl&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(Or &lt;code&gt;cjxl input.jpg output.jxl&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you ever need that exact same &lt;code&gt;.jpg&lt;/code&gt; file back (for an old app or for “bit-for-bit” proof), you use the decoder tool &lt;code&gt;djxl&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;djxl&lt;/span&gt;&lt;span&gt; output.jxl&lt;/span&gt;&lt;span&gt; restored.jpeg&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(Or &lt;code&gt;djxl output.jxl restored.jpg&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;To verify it worked, you can check their MD5 hash (a digital fingerprint). If the fingerprints match, the files are bit-for-bit identical:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;md5&lt;/span&gt;&lt;span&gt; input.jpeg&lt;/span&gt;&lt;span&gt; restored.jpeg&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Convert a PNG file:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cjxl&lt;/span&gt;&lt;span&gt; input.png&lt;/span&gt;&lt;span&gt; output.jxl&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;If you want it to be mathematically, pixel-perfectly lossless (exactly like your PNG but highly compressed and smaller), use the &lt;code&gt;-d&lt;/code&gt; (distance) flag set to 0:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cjxl&lt;/span&gt;&lt;span&gt; input.png&lt;/span&gt;&lt;span&gt; output.jxl&lt;/span&gt;&lt;span&gt; -d&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;The Ultimate Shortcut: Right-Click Finder Actions&lt;/h2&gt;
&lt;p&gt;For greater convenience, you can use macOS’s built-in &lt;strong&gt;Shortcuts&lt;/strong&gt; app to turn these command-line operations into Quick Actions in the Finder’s right-click menu, meaning you never have to open the Terminal again.&lt;/p&gt;
&lt;h3&gt;Step 1: Create the Shortcut&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;Shortcuts&lt;/strong&gt; app on your Mac.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;+&lt;/strong&gt; (plus) icon to create a new shortcut.&lt;/li&gt;
&lt;li&gt;In the right-hand sidebar, click the &lt;strong&gt;Shortcut Details&lt;/strong&gt; icon (the small “i” in a circle) and check the box &lt;strong&gt;“Use as Quick Action”&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Ensure &lt;strong&gt;“Finder”&lt;/strong&gt; is selected under the Quick Action settings.&lt;/li&gt;
&lt;li&gt;At the top of the main window, change the input to: &lt;em&gt;“Receive &lt;strong&gt;Images&lt;/strong&gt; from &lt;strong&gt;Quick Actions&lt;/strong&gt;.”&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 2: Add the “Run Shell Script” Action&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Search for the &lt;strong&gt;“Run Shell Script”&lt;/strong&gt; action in the sidebar and drag it into the shortcut.
&lt;blockquote&gt;
&lt;p&gt;By default, macOS disables scripting actions for security to prevent untrusted scripts from running automatically. You just need to flip a single toggle in the Shortcuts app settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Open Settings:&lt;/strong&gt; Click on the “Open Preferences” button directly in that error message, or go to the menu bar and select &lt;strong&gt;Shortcuts &amp;gt; Settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Go to Advanced:&lt;/strong&gt; Click on the &lt;strong&gt;Advanced&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable Scripts:&lt;/strong&gt; Check the box that says &lt;strong&gt;Allow Running Scripts&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;Set the shell to &lt;code&gt;/bin/zsh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;em&gt;Pass Input&lt;/em&gt; to &lt;code&gt;as arguments&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Fill in the code with the script below:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Include MacPorts and Homebrew (ARM/x86) paths&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; PATH&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;/opt/local/bin:/opt/homebrew/bin:/usr/local/bin:&lt;/span&gt;&lt;span&gt;$PATH&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;CJXL_PATH&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;$(&lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt; -v&lt;/span&gt;&lt;span&gt; cjxl)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Check if cjxl is installed&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;-z&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$CJXL_PATH&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    osascript&lt;/span&gt;&lt;span&gt; -e&lt;/span&gt;&lt;span&gt; &apos;display alert &quot;cjxl not found&quot; message &quot;Please make sure JPEG XL tools are installed (via `brew install jpeg-xl` or `sudo port install libjxl`).&quot;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    exit&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; f &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$@&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # Skip if already a JXL image&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    [[ &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ==&lt;/span&gt;&lt;span&gt; *&lt;/span&gt;&lt;span&gt;.jxl ]] &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;continue&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # Create the output filename by replacing the extension&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    output&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;${&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;}.jxl&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # Run the basic conversion&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;&lt;/span&gt;&lt;span&gt;$CJXL_PATH&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$output&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;done&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Try it out&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Name it something like &lt;strong&gt;“Convert to JXL”&lt;/strong&gt;, it should look like this:&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-03-05/convert-to-jxl-shortcuts.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Close the Shortcuts app.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Go to your &lt;strong&gt;Finder&lt;/strong&gt; and select any image (PNG, JPG, etc.).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Right-click the image, go down to &lt;strong&gt;Quick Actions&lt;/strong&gt;, and click &lt;strong&gt;Convert to JXL&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You may see a pop-up window the first time you run it:&lt;br /&gt;
&lt;strong&gt;“cjxl” would like to access files in your Downloads folder.&lt;/strong&gt;&lt;br /&gt;
Just choose &lt;strong&gt;Allow&lt;/strong&gt; this time and you will never see it again.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A small gear will spin in your menu bar, and seconds later, a highly-compressed &lt;code&gt;.jxl&lt;/code&gt; file will magically appear in the same folder as your original image.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Troubleshooting: Quick Action not showing up?&lt;/strong&gt;&lt;br /&gt;
Sometimes on newer Macs, the Quick Actions won’t appear in the right-click menu immediately. To fix this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check your settings: Click the &lt;strong&gt;Customize…&lt;/strong&gt; button in the Quick Actions menu, and make sure the new Shortcut is checked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relaunch Finder:&lt;/strong&gt; Hold the &lt;strong&gt;Option (⌥)&lt;/strong&gt; key, right-click the &lt;strong&gt;Finder&lt;/strong&gt; icon in your Dock, and click &lt;strong&gt;Relaunch&lt;/strong&gt;. Your new action should now appear!&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;How to Enable JPEG XL in Your Browser&lt;/h2&gt;
&lt;p&gt;Right now, Safari supports JPEG XL natively out-of-the-box on recent Apple devices. However, if you’re on Chrome, Brave, or Firefox, you can manually enable experimental flags to view JPEG XL images while default support continues to roll out.&lt;/p&gt;
&lt;h3&gt;Google Chrome &amp;amp; Brave&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open a new tab in your browser.&lt;/li&gt;
&lt;li&gt;In the address bar, type &lt;code&gt;chrome://flags&lt;/code&gt; (for Chrome) or &lt;code&gt;brave://flags&lt;/code&gt; (for Brave) and press &lt;strong&gt;Enter&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the search bar at the top of the page, type &lt;code&gt;jxl&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Locate the flag titled &lt;strong&gt;Enable JPEG XL image format&lt;/strong&gt; (or &lt;code&gt;#enable-jxl-image-format&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Click the dropdown menu next to it and change it from &lt;strong&gt;Default&lt;/strong&gt; to &lt;strong&gt;Enabled&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Relaunch&lt;/strong&gt; button to restart your browser.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Mozilla Firefox&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open a new tab and type &lt;code&gt;about:config&lt;/code&gt; in the address bar. Press &lt;strong&gt;Enter&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If prompted with a warning, click &lt;strong&gt;“Accept the Risk and Continue”&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the search box, type &lt;code&gt;image.jxl.enabled&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Double-click the entry (or click the toggle button) to change its value from &lt;code&gt;false&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;.
&lt;em&gt;(Alternatively, in recent versions, you can enable JPEG XL directly under &lt;strong&gt;Settings &amp;gt; Firefox Labs&lt;/strong&gt;.)&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Restart your browser.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Jebaraj, S. D., &amp;amp; N, S. (2023). JPEG-XL based Compression of DICOM Images for Reduced Storage and Transmission Costs. &lt;em&gt;2023 3rd International Conference on Intelligent Technologies (CONIT)&lt;/em&gt;, 1–6.&lt;br /&gt;
&lt;a href=&quot;https://doi.org/10.1109/conit59222.2023.10205928&quot;&gt;https://doi.org/10.1109/conit59222.2023.10205928&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Öztürk, E., &amp;amp; Mesut, A. (2021, September 1). Performance Evaluation of JPEG Standards, WebP and PNG in Terms of Compression Ratio and Time for Lossless Encoding. &lt;em&gt;IEEE Xplore&lt;/em&gt;.&lt;br /&gt;
&lt;a href=&quot;https://doi.org/10.1109/UBMK52708.2021.9558922&quot;&gt;https://doi.org/10.1109/UBMK52708.2021.9558922&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Blücher: Procedural Justice or Bureaucracy?</title><link>https://michifumi.de/blog/2026-02-26-blucher-procedural-justice-or-bureaucracy/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-02-26-blucher-procedural-justice-or-bureaucracy/</guid><description>A review of a Norwegian film released in 2025.</description><pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A review of a Norwegian film released in 2025.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;A low-budget yet high-quality film set during World War II. &lt;a href=&quot;https://www.imdb.com/title/tt32080656/&quot;&gt;IMDb&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The opening pace of the film is somewhat slow, primarily focusing on the protagonist’s domestic life, yet this effectively sets the stage for the heightened tension that unfolds during the war sequences later on. The battle scenes remain profoundly moving. One is struck by how truly accomplished directors can achieve effects rivalling Hollywood blockbusters on considerably smaller budgets.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;WARNING&lt;/p&gt;
&lt;p&gt;The following contains spoilers. If you haven’t watched this film yet, I’d strongly recommend doing so before coming back to this blog.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;Before watching this film, I had no idea that Bjørn Sundquist, who played Colonel Birger Eriksen (the commander of Oscarsborg Fortress who made the pivotal decision to open fire on the German heavy cruiser Blücher despite lacking clear orders), was such a talented actor. Especially in this scene:&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-02-26/Blucher-2025-0.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;Eriksen made the difficult decision to surrender to the German forces&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In this brief, about one-minute close-up of his face, lead actor Bjørn Sundquist masterfully conveys complex emotions that encapsulate the entire film’s tone. Much like the actor portraying the Joker in the second installment of Nolan’s Batman trilogy, it’s hard to imagine anyone surpassing his performance.&lt;/p&gt;
&lt;p&gt;Though the battle scenes were few, they were thoroughly thrilling. Especially when the shells struck the German warship — it even gave me the illusion of being right there in the thick of it.&lt;/p&gt;
&lt;p&gt;&lt;span&gt;&lt;img src=&quot;/2026-02-26/Blucher-2025-1.jxl&quot; alt=&quot;If you cannot see this image, either your browser does not support the JXL format, or it has been disabled by default. Click here to learn more.&quot; /&gt;&lt;/span&gt;
&lt;em&gt;The German heavy cruiser Blücher was hit by a 280mm Krupp gun&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Yet, what truly caught my attention in the plot, however, was not the combat scenes, but the Norwegian government’s accountability measures against Colonel Eriksen. Admittedly, he rashly ordered an attack without sufficient intelligence; admittedly, he surrendered to the Germans without any casualties in his troops despite orders to hold the fortress as long as possible, and signed the surrender document; admittedly, he must explain for this. But this does not mean the Norwegian government can take the high ground, shift all responsibility onto the colonel, and evade accountability itself. Is this accountability procedural justice? Undoubtedly. But is it fair? Clearly not. If the colonel is to be held accountable, then the incompetent, inept, and irresponsible government that was merely occupying positions without fulfilling its duties at the time should be held even more accountable.&lt;/p&gt;
&lt;p&gt;It is heartening that director Daniel Fahre has masterfully captured this bureaucracy — a term originating in Europe — on film, preserving it for posterity and prompting profound reflection on this chapter of history.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;So let’s return to the blog title: Was the Norwegian government’s accountability back then procedural justice or bureaucracy? I believe it was both.&lt;/p&gt;</content:encoded></item><item><title>FFmpeg: The Ultimate Cross-Platform Video Tool</title><link>https://michifumi.de/blog/2026-02-19-ffmpeg-the-ultimate-cross-platform-video-tool/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-02-19-ffmpeg-the-ultimate-cross-platform-video-tool/</guid><description>Why FFmpeg is my daily driver for video/audio processing and a collection of useful commands.</description><pubDate>Thu, 19 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Why FFmpeg is my daily driver for video/audio processing and a collection of useful commands.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;I’ve always maintained that only cross-platform software merits long-term commitment. Final Cut Pro is indeed formidable, and Adobe’s suite is undeniably capable, but neither runs natively on every operating system. Once you’ve grown accustomed to them, you’ve effectively tied yourself to the systems they run on. Departing from a particular platform means abandoning these familiar tools, significantly increasing your sunk costs.&lt;/p&gt;
&lt;p&gt;But fear not, &lt;a href=&quot;https://ffmpeg.org/&quot;&gt;FFmpeg&lt;/a&gt; covers our asses.&lt;/p&gt;
&lt;p&gt;I started using it by processing some simple tasks, such as trimming the duration of video or audio clips, extracting segments, merging multiple clips, changing the format of videos (e.g. .mkv to .mp4) to make them more compatible with different devices, re-encoding video and audio (e.g. H.264 to HEVC or Opus to AAC), compressing videos, changing their resolution and burning subtitles into videos.&lt;/p&gt;
&lt;p&gt;These tasks are a daily driver for me at certain times, but they are not worth processing by launching a large app like Final Cut Pro. After some time of learning and hands-on practice, I’ve compiled a list of frequently used commands for reference.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Basic Trimming&lt;/h2&gt;
&lt;p&gt;Trimming a video without re-encoding is extremely fast as it simply copies the compressed data without decoding.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep everything from 10 minutes onwards:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -t&lt;/span&gt;&lt;span&gt; 00:10:00&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -c&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Extract a 20-minute segment starting from 10:00:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -ss&lt;/span&gt;&lt;span&gt; 00:10:00&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -t&lt;/span&gt;&lt;span&gt; 00:20:00&lt;/span&gt;&lt;span&gt; -c&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Trim the first 20 minutes:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -ss&lt;/span&gt;&lt;span&gt; 00:20:00&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -c&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;Stream copying (&lt;code&gt;-c copy&lt;/code&gt;) can only cut on keyframes (I-frames / IDR frames). If &lt;code&gt;10:00&lt;/code&gt; is not an exact keyframe, FFmpeg seeks to the nearest preceding keyframe, which may make the output start slightly earlier or cause brief frozen frames in some video players. For frame-accurate cutting down to the millisecond, re-encode by removing &lt;code&gt;-c copy&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you want to change the minutes or seconds, simply adjust the numbers in the command. However, if the time exceeds 1 hour, use the format &lt;code&gt;HH:MM:SS&lt;/code&gt; (e.g., &lt;code&gt;1:10:00&lt;/code&gt; instead of &lt;code&gt;70:00&lt;/code&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Merging and Concatenation&lt;/h2&gt;
&lt;p&gt;If you have multiple clips with the same parameters (resolution, codec, etc.), you can merge them using the concat demuxer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Merge clips listed in &lt;code&gt;merge.txt&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; concat&lt;/span&gt;&lt;span&gt; -safe&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; merge.txt&lt;/span&gt;&lt;span&gt; -c&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;merge.txt&lt;/code&gt; should contain lines formatted as &lt;code&gt;file &apos;input.mp4&apos;&lt;/code&gt;. You can download a &lt;a href=&quot;/merge.txt&quot;&gt;template here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Merge clips without a text file (Shell inline list):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; concat&lt;/span&gt;&lt;span&gt; -safe&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; &amp;lt;(&lt;/span&gt;&lt;span&gt;printf&lt;/span&gt;&lt;span&gt; &quot;file &apos;%s&apos;\n&quot; input1.mp4 input2.mp4)&lt;/span&gt;&lt;span&gt; -c&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On macOS or Linux (&lt;code&gt;zsh&lt;/code&gt;/&lt;code&gt;bash&lt;/code&gt;), process substitution (&lt;code&gt;&amp;lt;(...)&lt;/code&gt;) feeds the file list directly from memory. This saves you from creating a physical &lt;code&gt;merge.txt&lt;/code&gt; file on disk while keeping the blazing-fast, lossless stream copy (&lt;code&gt;-c copy&lt;/code&gt;). All clips must still have identical codecs and parameters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Merge clips with different parameters (&lt;code&gt;concat&lt;/code&gt; filter):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input1.mp4&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input2.mp4&lt;/span&gt;&lt;span&gt; -filter_complex&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[v][a]&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;-map &lt;/span&gt;&lt;span&gt;&quot;[v]&quot;&lt;/span&gt;&lt;span&gt; -map&lt;/span&gt;&lt;span&gt; &quot;[a]&quot;&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unlike the concat demuxer, the &lt;code&gt;concat&lt;/code&gt; filter decodes and re-encodes the streams. This eliminates the need for an external file and seamlessly handles clips with different resolutions, framerates, or codecs, though it will take longer to encode.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Side-by-side comparison with audio (1080p, High Quality):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input1.mov&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input2.mp4&lt;/span&gt;&lt;span&gt; -filter_complex&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;[0:v]fps=60,scale=-2:1080[v0]; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; [1:v]fps=60,scale=-2:1080[v1]; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; [v0][v1]hstack=inputs=2[v]&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;-map &lt;/span&gt;&lt;span&gt;&quot;[v]&quot;&lt;/span&gt;&lt;span&gt; -map&lt;/span&gt;&lt;span&gt; 1:a&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; libx264&lt;/span&gt;&lt;span&gt; -crf&lt;/span&gt;&lt;span&gt; 19&lt;/span&gt;&lt;span&gt; -preset&lt;/span&gt;&lt;span&gt; medium&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac&lt;/span&gt;&lt;span&gt; -b:a&lt;/span&gt;&lt;span&gt; 129k&lt;/span&gt;&lt;span&gt; -pix_fmt&lt;/span&gt;&lt;span&gt; yuv420p&lt;/span&gt;&lt;span&gt; -shortest&lt;/span&gt;&lt;span&gt; output_side_by_side_hq.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Scales both inputs to 1080p height and places them side-by-side horizontally using &lt;code&gt;hstack&lt;/code&gt;. It retains the audio track from the second video (&lt;code&gt;-map 1:a&lt;/code&gt;), balances high quality with reasonable file size (&lt;code&gt;-crf 19&lt;/code&gt;), and trims output to the shorter video (&lt;code&gt;-shortest&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Side-by-side comparison for desktop / text clarity (1800p, Near-lossless):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input1.mov&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input2.mp4&lt;/span&gt;&lt;span&gt; -filter_complex&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&quot;[0:v]fps=60,scale=-2:1800:flags=lanczos[v0]; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; [1:v]fps=60,scale=-2:1800:flags=lanczos[v1]; &lt;/span&gt;&lt;span&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt; [v0][v1]hstack=inputs=2[v]&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;-map &lt;/span&gt;&lt;span&gt;&quot;[v]&quot;&lt;/span&gt;&lt;span&gt; -an&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;-c:v &lt;/span&gt;&lt;span&gt;libx264&lt;/span&gt;&lt;span&gt; -crf&lt;/span&gt;&lt;span&gt; 12&lt;/span&gt;&lt;span&gt; -preset&lt;/span&gt;&lt;span&gt; veryslow&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;-x264-params &lt;/span&gt;&lt;span&gt;&quot;no-deblock=1:aq-mode=3:qcomp=0.8&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;-pix_fmt &lt;/span&gt;&lt;span&gt;yuv420p&lt;/span&gt;&lt;span&gt; -shortest&lt;/span&gt;&lt;span&gt; output_ultra.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ideal for screen recording comparisons where text and UI sharpness matter. It scales to 1800p using the sharper &lt;code&gt;lanczos&lt;/code&gt; algorithm, strips audio (&lt;code&gt;-an&lt;/code&gt;), and disables in-loop deblocking (&lt;code&gt;no-deblock=1&lt;/code&gt;) with &lt;code&gt;-crf 12&lt;/code&gt; to prevent fine UI details and fonts from being smoothed out.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. Encoding for Compatibility&lt;/h2&gt;
&lt;p&gt;Sometimes you need to ensure a video plays everywhere (QuickTime, Safari, iOS, smart TVs, and web browsers) by using standard H.264 settings with 8-bit YUV 4:2:0 chroma subsampling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;High-quality H.264 re-encode for universal playback and storage:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; libx264&lt;/span&gt;&lt;span&gt; -crf&lt;/span&gt;&lt;span&gt; 18&lt;/span&gt;&lt;span&gt; -preset&lt;/span&gt;&lt;span&gt; veryslow&lt;/span&gt;&lt;span&gt; -pix_fmt&lt;/span&gt;&lt;span&gt; yuv420p&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac&lt;/span&gt;&lt;span&gt; -b:a&lt;/span&gt;&lt;span&gt; 129k&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; avc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;Use this carefully, this performs CPU-heavy software encoding. While &lt;code&gt;-crf 18&lt;/code&gt; with &lt;code&gt;-preset veryslow&lt;/code&gt; delivers near-lossless visual quality, it can be quite slow. For everyday use, &lt;code&gt;-crf 23&lt;/code&gt; with &lt;code&gt;-preset medium&lt;/code&gt; provides an excellent balance of speed and quality. The &lt;code&gt;-pix_fmt yuv420p&lt;/code&gt; flag is critical: without it, sources with 10-bit color, 4:4:4 chroma, or RGB color (common in screen recordings and image sequences) will produce high-profile streams that Apple devices and web browsers cannot play.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Burning Subtitles (Hardsubs)&lt;/h2&gt;
&lt;p&gt;Burning subtitles directly into the video stream ensures they show up on any player.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Basic subtitle burn-in:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -vf&lt;/span&gt;&lt;span&gt; &quot;subtitles=subtitle.srt&quot;&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;When declaring filters, it is better to quote the entire filter string, i.e., &lt;code&gt;-vf &quot;subtitles=subtitle.srt&quot;&lt;/code&gt; rather than &lt;code&gt;-vf subtitles=&quot;subtitle.srt&quot;&lt;/code&gt;. This ensures the shell correctly passes the entire string as a single argument to the &lt;code&gt;-vf&lt;/code&gt; option.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Burn subtitles with spaces in the filename (Best Practice):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mkv&lt;/span&gt;&lt;span&gt; -vf&lt;/span&gt;&lt;span&gt; &quot;subtitles=&apos;my subtitles.srt&apos;&quot;&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; output.mkv&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;If your subtitle filename has spaces or special characters, nest single quotes inside the double quotes holding the filter.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Burn VTT subtitles with a specific font (Songti SC):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -vf&lt;/span&gt;&lt;span&gt; &quot;subtitles=subtitle.vtt:force_style=&apos;FontName=Songti SC&apos;&quot;&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; libx264&lt;/span&gt;&lt;span&gt; -crf&lt;/span&gt;&lt;span&gt; 18&lt;/span&gt;&lt;span&gt; -preset&lt;/span&gt;&lt;span&gt; veryslow&lt;/span&gt;&lt;span&gt; -pix_fmt&lt;/span&gt;&lt;span&gt; yuv420p&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac&lt;/span&gt;&lt;span&gt; -b:a&lt;/span&gt;&lt;span&gt; 129k&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; avc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;FFmpeg’s &lt;code&gt;subtitles&lt;/code&gt; filter also fully supports &lt;code&gt;.srt&lt;/code&gt; and &lt;code&gt;.ass&lt;/code&gt; formats. While &lt;code&gt;.vtt&lt;/code&gt; and &lt;code&gt;.srt&lt;/code&gt; may require &lt;code&gt;force_style&lt;/code&gt; to look good, &lt;code&gt;.ass&lt;/code&gt; files (Advanced SubStation Alpha) can contain their own rich styling, colors, and positioning data which FFmpeg will render perfectly out of the box. Note that &lt;code&gt;Songti SC&lt;/code&gt; is a macOS system font, on Linux or Windows, replace it with an installed font such as &lt;code&gt;Noto Serif CJK SC&lt;/code&gt; or &lt;code&gt;SimSun&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Format Conversion and Optimization&lt;/h2&gt;
&lt;p&gt;Converting between formats like WebM to MP4 or using modern codecs like HEVC (H.265).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compress a video to 720p MP4 (H.264) &lt;em&gt;with audio re-encoding&lt;/em&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -vf&lt;/span&gt;&lt;span&gt; scale=1280:720&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; libx264&lt;/span&gt;&lt;span&gt; -crf&lt;/span&gt;&lt;span&gt; 23&lt;/span&gt;&lt;span&gt; -preset&lt;/span&gt;&lt;span&gt; slow&lt;/span&gt;&lt;span&gt; -pix_fmt&lt;/span&gt;&lt;span&gt; yuv420p&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac&lt;/span&gt;&lt;span&gt; -b:a&lt;/span&gt;&lt;span&gt; 129k&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; avc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Compress a video to 10-bit 1080p MP4 (H.265) &lt;em&gt;with original audio codec&lt;/em&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -vf&lt;/span&gt;&lt;span&gt; scale=1920:1080&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; libx265&lt;/span&gt;&lt;span&gt; -crf&lt;/span&gt;&lt;span&gt; 28&lt;/span&gt;&lt;span&gt; -preset&lt;/span&gt;&lt;span&gt; slow&lt;/span&gt;&lt;span&gt; -pix_fmt&lt;/span&gt;&lt;span&gt; yuv420p10le&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; hvc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;If you use &lt;code&gt;-vf scale=1280:-2&lt;/code&gt;, FFmpeg will fix the width at 1280 and automatically calculate the height to preserve the original aspect ratio while guaranteeing the height is divisible by 2 (avoiding “height not divisible by 2” encoder errors with YUV 4:2:0). The commands above force fixed dimensions (1280:720 or 1920:1080), which will distort the aspect ratio if the input isn’t already 16:9.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;6. macOS Hardware Acceleration (VideoToolbox)&lt;/h2&gt;
&lt;p&gt;If you’re on a Mac, using &lt;code&gt;videotoolbox&lt;/code&gt; will significantly speed up the encoding process and save battery.&lt;/p&gt;











&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;💡 GPU Compatibility Note&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Hardware acceleration is split between &lt;strong&gt;decoding&lt;/strong&gt; (reading inputs via &lt;code&gt;-hwaccel videotoolbox&lt;/code&gt;) and &lt;strong&gt;encoding&lt;/strong&gt; (writing outputs via &lt;code&gt;-c:v h264_videotoolbox&lt;/code&gt; or &lt;code&gt;hevc_videotoolbox&lt;/code&gt;). While H.264 and HEVC hardware decode/encode are supported across almost all modern Macs, formats like &lt;strong&gt;AV1&lt;/strong&gt; hardware decoding are only available on Apple &lt;strong&gt;M3&lt;/strong&gt; chips or newer. If your Mac does not support hardware decoding for a specific input format, simply omit &lt;code&gt;-hwaccel videotoolbox&lt;/code&gt;, FFmpeg will decode smoothly on the CPU while still using VideoToolbox for fast hardware encoding.&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Fast H.264 and Apple native AAC re-encoding:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -hwaccel&lt;/span&gt;&lt;span&gt; videotoolbox&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.webm&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; h264_videotoolbox&lt;/span&gt;&lt;span&gt; -b:v&lt;/span&gt;&lt;span&gt; 5000k&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac_at&lt;/span&gt;&lt;span&gt; -q:a&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; avc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fast 10-bit HEVC (H.265) and Apple native AAC re-encoding:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -hwaccel&lt;/span&gt;&lt;span&gt; videotoolbox&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.webm&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; hevc_videotoolbox&lt;/span&gt;&lt;span&gt; -b:v&lt;/span&gt;&lt;span&gt; 3000k&lt;/span&gt;&lt;span&gt; -pix_fmt&lt;/span&gt;&lt;span&gt; p010le&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac_at&lt;/span&gt;&lt;span&gt; -q:a&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; hvc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;HEVC and &lt;code&gt;aac_at&lt;/code&gt; with Burned Subtitles:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.webm&lt;/span&gt;&lt;span&gt; -vf&lt;/span&gt;&lt;span&gt; subtitles=subtitle.vtt&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; hevc_videotoolbox&lt;/span&gt;&lt;span&gt; -b:v&lt;/span&gt;&lt;span&gt; 2500k&lt;/span&gt;&lt;span&gt; -pix_fmt&lt;/span&gt;&lt;span&gt; p010le&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac_at&lt;/span&gt;&lt;span&gt; -q:a&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; hvc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;When burning subtitles with the &lt;code&gt;subtitles&lt;/code&gt; filter, FFmpeg processes frames on the CPU in software memory, so omitting &lt;code&gt;-hwaccel videotoolbox&lt;/code&gt; here is expected.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;H.264 with Burned Subtitles (Custom Font for Chinese):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.webm&lt;/span&gt;&lt;span&gt; -vf&lt;/span&gt;&lt;span&gt; &quot;subtitles=subtitle.vtt:force_style=&apos;FontName=Songti SC&apos;&quot;&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; h264_videotoolbox&lt;/span&gt;&lt;span&gt; -b:v&lt;/span&gt;&lt;span&gt; 4000k&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac_at&lt;/span&gt;&lt;span&gt; -q:a&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; avc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;HEVC with Burned Subtitles (Custom Font for Chinese):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.webm&lt;/span&gt;&lt;span&gt; -vf&lt;/span&gt;&lt;span&gt; &quot;subtitles=subtitle.vtt:force_style=&apos;FontName=Songti SC&apos;&quot;&lt;/span&gt;&lt;span&gt; -c:v&lt;/span&gt;&lt;span&gt; hevc_videotoolbox&lt;/span&gt;&lt;span&gt; -pix_fmt&lt;/span&gt;&lt;span&gt; p010le&lt;/span&gt;&lt;span&gt; -b:v&lt;/span&gt;&lt;span&gt; 2500k&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac_at&lt;/span&gt;&lt;span&gt; -q:a&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; -tag:v&lt;/span&gt;&lt;span&gt; hvc1&lt;/span&gt;&lt;span&gt; output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Audio Extraction&lt;/h2&gt;
&lt;p&gt;Extracting high-quality audio from video files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extract audio to M4A without re-encoding (Fastest, Original Quality):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -vn&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; copy&lt;/span&gt;&lt;span&gt; output.m4a&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;Stream copying (&lt;code&gt;-c:a copy&lt;/code&gt;) into &lt;code&gt;.m4a&lt;/code&gt; requires the source audio to already be an MP4-compatible format (typically AAC or ALAC). If the video contains Opus, Vorbis, or DTS, re-encode it using the commands below or extract into its native container (e.g. &lt;code&gt;output.opus&lt;/code&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Extract audio to M4A using &lt;code&gt;libfdk_aac&lt;/code&gt; re-encoding (VBR scale: 1 - 5, 5 is the highest quality):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -vn&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; libfdk_aac&lt;/span&gt;&lt;span&gt; -vbr&lt;/span&gt;&lt;span&gt; 5&lt;/span&gt;&lt;span&gt; output.m4a&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Extract audio to M4A using &lt;code&gt;aac_at&lt;/code&gt; re-encoding (macOS Native, 0 - 14, 0 is the highest quality):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ffmpeg&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;span&gt; input.mp4&lt;/span&gt;&lt;span&gt; -vn&lt;/span&gt;&lt;span&gt; -c:a&lt;/span&gt;&lt;span&gt; aac_at&lt;/span&gt;&lt;span&gt; -q:a&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; output.m4a&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;Notice the quality scale direction! Unlike &lt;code&gt;libfdk_aac&lt;/code&gt; where &lt;code&gt;5&lt;/code&gt; is the highest quality, Apple’s &lt;code&gt;aac_at&lt;/code&gt; uses a reverse scale where &lt;code&gt;0&lt;/code&gt; is the highest quality (~192 kbps) and &lt;code&gt;14&lt;/code&gt; is the lowest. Use &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;, or &lt;code&gt;2&lt;/code&gt; for clean, high-fidelity sound.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h3&gt;Advice for macOS Users (Homebrew &amp;amp; Audio Encoders)&lt;/h3&gt;
&lt;p&gt;If you find that some of these commands fit your requirements, or if you’re interested in exploring more of what FFmpeg has to offer and are ready to install it on your Mac, here is some practical guidance.&lt;/p&gt;
&lt;p&gt;First, an important clarification: &lt;strong&gt;you do not need third-party taps just to get high-quality AAC audio on macOS&lt;/strong&gt;. Standard Homebrew FFmpeg (&lt;code&gt;brew install ffmpeg&lt;/code&gt;) already has Apple’s native &lt;code&gt;aac_at&lt;/code&gt; (AudioToolbox AAC) enabled out of the box. &lt;code&gt;aac_at&lt;/code&gt; is widely regarded as one of the best AAC encoders available, rivaling or exceeding &lt;code&gt;libfdk_aac&lt;/code&gt; without compiling anything from source.&lt;/p&gt;
&lt;p&gt;However, if you want non-free libraries like Fraunhofer’s FDK AAC (&lt;code&gt;libfdk_aac&lt;/code&gt;) for cross-platform script parity, or other optional features not included in core Homebrew even with &lt;code&gt;brew install ffmpeg-full&lt;/code&gt;, you can use the &lt;a href=&quot;https://github.com/homebrew-ffmpeg/homebrew-ffmpeg&quot;&gt;homebrew-ffmpeg/homebrew-ffmpeg&lt;/a&gt; tap. According to the &lt;a href=&quot;https://trac.ffmpeg.org/wiki/CompilationGuide/macOS&quot;&gt;official FFmpeg macOS compilation guide&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since v2.0, Homebrew does not offer options for its core formulae anymore. Users who want to build ffmpeg with additional libraries (including non-free ones) need to use so-called taps from third party repositories. These repositories are not maintained by Homebrew.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;To install FFmpeg with custom options via the tap:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; tap&lt;/span&gt;&lt;span&gt; homebrew-ffmpeg/ffmpeg&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; homebrew-ffmpeg/ffmpeg/ffmpeg&lt;/span&gt;&lt;span&gt; --with-&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;option&lt;/span&gt;&lt;span&gt;1&amp;gt;&lt;/span&gt;&lt;span&gt; --with-&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;option&lt;/span&gt;&lt;span&gt;2&amp;gt;&lt;/span&gt;&lt;span&gt; ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example, to build with &lt;code&gt;libfdk_aac&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; homebrew-ffmpeg/ffmpeg/ffmpeg&lt;/span&gt;&lt;span&gt; --with-libfdk-aac&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Troubleshooting&lt;/h3&gt;
&lt;p&gt;While building with custom options increases flexibility, it comes with a maintenance cost. After running &lt;code&gt;brew upgrade&lt;/code&gt;, you may occasionally notice your custom FFmpeg build breaking with dynamic linker &lt;code&gt;dyld&lt;/code&gt; errors. This happens when Homebrew updates a shared library dependency (such as &lt;code&gt;x264&lt;/code&gt; or &lt;code&gt;openssl&lt;/code&gt;) to a new version path, leaving your custom-compiled FFmpeg linked to the old, deleted path.&lt;/p&gt;
&lt;p&gt;Fortunately, it is easy to repair. Simply reinstall FFmpeg to recompile it against the updated libraries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; reinstall&lt;/span&gt;&lt;span&gt; homebrew-ffmpeg/ffmpeg/ffmpeg&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Homebrew remembers the &lt;code&gt;--with-*&lt;/code&gt; options you originally selected and reapplies them during the reinstall.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;
  &lt;div&gt;🏆&lt;/div&gt;
  &lt;h2&gt;Congratulations!&lt;/h2&gt;
  &lt;p&gt;You have officially earned the title of:&lt;/p&gt;
  &lt;div&gt;
    FFmpeg Ruler
  &lt;/div&gt;
  &lt;div&gt;Certified by Michifumi&lt;/div&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;In the next blog, I’ll introduce another free software that can cooperate with FFmpeg and make it even stronger.&lt;/p&gt;</content:encoded></item><item><title>A Testament to Courage: 2000 Metres to Andriivka</title><link>https://michifumi.de/blog/2026-02-09-a-testament-to-courage-2000-metres-to-andriivka/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-02-09-a-testament-to-courage-2000-metres-to-andriivka/</guid><description>A raw, unflinching look at the brutal reality of modern warfare through the eyes of Ukrainian soldiers.</description><pubDate>Mon, 09 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A raw, unflinching look at the brutal reality of modern warfare through the eyes of Ukrainian soldiers.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;I finished watching the documentary &lt;a href=&quot;https://www.imdb.com/title/tt34964205/&quot;&gt;2000 Metres to Andriivka&lt;/a&gt; today.&lt;/p&gt;
&lt;p&gt;To be honest, it was hard to watch. I don’t even remember how many times I pressed pause during the first ten minutes, especially during the first final scene, I felt desperate just looking at the screen. I can’t even express my complex emotions in words.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;English&lt;/h2&gt;
&lt;p&gt;But as I mustered the courage to keep watching, I realised that opening scene was far from the beginning of hell. Although I’ve seen plenty of footage from drone perspectives showing buildings destroyed, tanks blown up, and even attacks on Russian soldiers, those videos always cut off the instant the drone explodes. This time, however, I witness the battlefield through the first-person perspective of helmet-mounted cameras worn by brave Ukrainian soldiers.&lt;/p&gt;
&lt;p&gt;I follow the courageous Ukrainian fighters as I hear their battle cries and anguished shouts, feel the whistling of bullets tearing through the air, experience the shockwaves of bombs exploding nearby, and see fallen brothers who never rise again… That constant dread of what horrors might unfold in the next second, even through the screen, does nothing to ease my taut nerves. I find myself hitting the pause button more and more frequently, trying to suppress the rising nausea.&lt;/p&gt;
&lt;p&gt;Trust me, even if you think you’re desensitised to &lt;em&gt;Saving Private Ryan&lt;/em&gt;-level scenes, you have no idea what you’re about to face.&lt;/p&gt;
&lt;p&gt;War, war never changes. Perhaps in my lifetime, I may not witness humanity breaking free from the painful cycle of war, but I have never ceased to envision a beautiful future where mankind eradicates war and advances hand in hand.&lt;/p&gt;
&lt;h2&gt;中文&lt;/h2&gt;
&lt;p&gt;可随着我鼓起勇气继续观看，我才发现我刚刚只是站在地狱的入口处。虽然我从无人机视角看过不少摧毁建筑，炸毁坦克，甚至攻击俄军士兵的场景，但那些视频在无人机爆炸的钱一瞬间就断掉了。而这次是通过士兵身上携带的头盔摄像头，以第一人称的视角，跟随英勇的乌克兰战士来观测战场。&lt;/p&gt;
&lt;p&gt;战士们的怒吼，哀嚎，子弹在空中划过的声音，炸弹在身旁爆炸的震撼，倒下的战友再也没能站起来…… 我每时每刻都不在担心着下一秒又会有什么坏事发生，哪怕隔着屏幕，也丝毫无法缓和我紧绷的神经。我只能更加频繁地按下暂定键，试图压下那反胃的感觉。&lt;/p&gt;
&lt;p&gt;相信我，就算你认为你已经适应了《拯救大兵瑞恩》级别的场景，你也并不知道自己将要面临什么。&lt;/p&gt;
&lt;p&gt;War, war never changes. 或许在我的有生之年，无法看到人类脱离战争的痛苦轮回，但我从未停止对人类根绝战争，携手共进的美好未来的畅想。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This documentary, produced and directed by Mstyslav Chernov, was released on 23 January 2025. I believe the Nobel Peace Prize should be awarded to individuals like those who courageously risk their lives on the front lines to defence their homeland, document and expose the brutal realities of war to the world.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;人間讃歌は「勇気」の讃歌ッ！！人間のすばらしさは勇気のすばらしさ！！&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;Thank you for reading. Please watch the documentary when you are ready. It’s worth experiencing the pain.&lt;/p&gt;
&lt;p&gt;If you’re interested in learning more about Ukraine’s history, I recommend another documentary: &lt;a href=&quot;https://www.imdb.com/title/tt4908644/&quot;&gt;Winter on Fire&lt;/a&gt;, which chronicles the events of 2013-2014, and &lt;a href=&quot;https://www.imdb.com/title/tt24082438/&quot;&gt;20 Days in Mariupol&lt;/a&gt;, which chronicles the events of 2022.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Слава Україні! 🇺🇦&lt;/p&gt;</content:encoded></item><item><title>How to Remove EXIF Metadata from Images on macOS</title><link>https://michifumi.de/blog/2026-02-04-how-to-remove-exif-metadata-from-images-on-macos/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-02-04-how-to-remove-exif-metadata-from-images-on-macos/</guid><description>Scrubbing EXIF data protects your privacy by stripping sensitive location and time info, removing digital fingerprints that link photos back to you.</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Scrubbing EXIF data protects your privacy by stripping sensitive location and time info, removing digital fingerprints that link photos back to you.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;Have you ever considered that your photo might contain more information than you could imagine? The information contained within a photo or image extends far beyond what meets the eye, and a lot of that data leaks through EXIF.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Exif&quot;&gt;EXIF data&lt;/a&gt; (Exchangeable Image File Format), is metadata embedded in digital images and videos that records detailed technical and contextual information about how and when the media was created. It can contain camera and equipment details, shooting settings, temporal information, geolocation data, image characteristics, software and processing info, copyright and ownership, and so on, you will be surprised how much metadata can be embedded into a single image.&lt;/p&gt;
&lt;p&gt;If such photos or images are only sitting on your local hard drive, it’s not a big deal. But right after you want to share them on the internet, such as posting them on your personal blog or social media, that’s where bad things can happen. Although most major social media platforms, like Instagram or X, automatically strip EXIF data when you upload, that also means they can access your metadata, refine your user profile to deliver targeted advertising, or sell your personal information to third parties if they want to (and trust me, they will).&lt;/p&gt;
&lt;p&gt;The worst situation is when you send an image to a forum, a comment section, or an online public storage service that most likely doesn’t have the functionality to scrub metadata and allows everyone to download the original file. Then you are in big trouble. Strangers can use this information to track you to your workplace, the parks where you feed pigeons, or even to your home. Just imagine how scary that would be.&lt;/p&gt;
&lt;p&gt;That’s why you need to make sure every piece of data inside a photo or image is wiped out before it reaches the internet. This blog will guide you on how to easily scrub all the EXIF metadata from an image on macOS, using &lt;a href=&quot;https://exiftool.org/&quot;&gt;ExifTool by Phil Harvey&lt;/a&gt; and the built-in Shortcuts app. By the end of this tutorial, you will be able to scrub an image with just one click.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-02-04/exiftool-quick-action.png&quot; alt=&quot;exiftool quick action&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 0: Install ExifTool&lt;/h2&gt;
&lt;p&gt;First, we need to install ExifTool on the device. If you don’t have Homebrew on your Mac, install it using the following command:&lt;/p&gt;
&lt;h3&gt;1. Install Homebrew&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/bin/bash&lt;/span&gt;&lt;span&gt; -c&lt;/span&gt;&lt;span&gt; &quot;$(&lt;/span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -fsSL&lt;/span&gt;&lt;span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Install ExifTool&lt;/h3&gt;
&lt;p&gt;Open your Terminal and run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; exiftool&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 1: Create a New Shortcut&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Open the &lt;strong&gt;Shortcuts&lt;/strong&gt; app on your Mac.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;+&lt;/strong&gt; (plus) icon to create a new shortcut.&lt;/li&gt;
&lt;li&gt;In the right-hand sidebar, click the &lt;strong&gt;Shortcut Details&lt;/strong&gt; icon (the small “i” in a circle) and check the box &lt;strong&gt;“Use as Quick Action”&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Check the box for &lt;strong&gt;“Finder”&lt;/strong&gt; under the Quick Action settings.&lt;/li&gt;
&lt;li&gt;At the very top of the main window, change the input to: &lt;em&gt;“Receive &lt;strong&gt;Image&lt;/strong&gt; from &lt;strong&gt;Quick Actions&lt;/strong&gt;.”&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 2: Add the Shell Script&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Search for the &lt;strong&gt;“Run Shell Script”&lt;/strong&gt; action in the right sidebar and drag it into your shortcut.
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;By default, macOS disables scripting actions for security to prevent untrusted scripts from running automatically. You just need to flip a single toggle in the Shortcuts app settings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go to the menu bar and select &lt;strong&gt;Shortcuts &amp;gt; Settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click on the &lt;strong&gt;Advanced&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Check the box that says &lt;strong&gt;Allow Running Scripts&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;Set the &lt;em&gt;Shell&lt;/em&gt; to &lt;code&gt;/bin/zsh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;em&gt;Pass Input&lt;/em&gt; to &lt;code&gt;as arguments&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Paste the following script:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Include MacPorts and Homebrew (ARM/x86) paths&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; PATH&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;/opt/local/bin:/opt/homebrew/bin:/usr/local/bin:&lt;/span&gt;&lt;span&gt;$PATH&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EXIFTOOL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;$(&lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt; -v&lt;/span&gt;&lt;span&gt; exiftool)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Check if exiftool is installed&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;-z&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$EXIFTOOL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    osascript&lt;/span&gt;&lt;span&gt; -e&lt;/span&gt;&lt;span&gt; &apos;display alert &quot;exiftool not found&quot; message &quot;Please make sure exiftool is installed (via `brew install exiftool` or `sudo port install exiftool`).&quot;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    exit&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 0. Trigger macOS permission prompt&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;osascript&lt;/span&gt;&lt;span&gt; -e&lt;/span&gt;&lt;span&gt; &quot;tell application &lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;Finder&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt; to duplicate file POSIX file &lt;/span&gt;&lt;span&gt;\&quot;$1\&quot;&lt;/span&gt;&lt;span&gt; to POSIX file &lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt;/tmp/&lt;/span&gt;&lt;span&gt;\&quot;&lt;/span&gt;&lt;span&gt; with replacing&quot;&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; /dev/null&lt;/span&gt;&lt;span&gt; 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;span&gt; &amp;amp;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;OSPID&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$!&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 1. Wait until permission is granted (retries every second, up to 30s)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;RETRY&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;while&lt;/span&gt;&lt;span&gt; [ $RETRY &lt;/span&gt;&lt;span&gt;-lt&lt;/span&gt;&lt;span&gt; 30&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    RESULT&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$EXIFTOOL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; -all=&lt;/span&gt;&lt;span&gt; -overwrite_original&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; !&lt;/span&gt;&lt;span&gt; echo&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$RESULT&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; grep&lt;/span&gt;&lt;span&gt; -q&lt;/span&gt;&lt;span&gt; &quot;Error opening file&quot;&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        break&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fi&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    sleep&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    RETRY&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;$((&lt;/span&gt;&lt;span&gt;RETRY&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;done&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 2. Clean up the background trigger&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;kill&lt;/span&gt;&lt;span&gt; $OSPID &lt;/span&gt;&lt;span&gt;2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;wait&lt;/span&gt;&lt;span&gt; $OSPID &lt;/span&gt;&lt;span&gt;2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;rm&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; &quot;/tmp/$(&lt;/span&gt;&lt;span&gt;basename&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$1&lt;/span&gt;&lt;span&gt;&quot;)&quot;&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; /dev/null&lt;/span&gt;&lt;span&gt; 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$RESULT&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# 3. Process remaining files and show results&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; f &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$@&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; !=&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$1&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &quot;&lt;/span&gt;&lt;span&gt;$EXIFTOOL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; -all=&lt;/span&gt;&lt;span&gt; -overwrite_original&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    fi&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    echo&lt;/span&gt;&lt;span&gt; &quot;--- FILE: $(&lt;/span&gt;&lt;span&gt;basename&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;) ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;&lt;/span&gt;&lt;span&gt;$EXIFTOOL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    echo&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;done&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why set the &lt;code&gt;PATH&lt;/code&gt; manually?&lt;/strong&gt; macOS’s Shortcuts action runs in a minimal environment without sourcing your shell configuration (like &lt;code&gt;~/.zshrc&lt;/code&gt;), so package manager directories aren’t in &lt;code&gt;$PATH&lt;/code&gt; by default. Exporting standard paths ensures the shortcut works out of the box regardless of whether ExifTool was installed via Homebrew (Apple Silicon or Intel), MacPorts, or the official macOS package installer. If ExifTool is not installed, it displays a helpful alert popup.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What does the &lt;code&gt;osascript&lt;/code&gt; line do?&lt;/strong&gt; macOS’s Shortcuts sandbox silently blocks command-line tools like ExifTool from accessing files in protected folders (Downloads, Desktop, Documents). This clever one-liner asks Finder to touch the file via AppleScript, which forces macOS to show a permission prompt. The first time you run the shortcut, a dialog will appear asking &lt;em&gt;“Finder.app” would like to access files in your Downloads folder&lt;/em&gt;. Click &lt;strong&gt;Allow&lt;/strong&gt;, and the script will automatically retry and succeed. This only happens once — all future runs will work instantly.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3: Add the Pop-Up Notification&lt;/h2&gt;
&lt;p&gt;Since a shell script runs in the background, you won’t see the output unless we tell Shortcuts to display it.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In the right sidebar, search for the &lt;strong&gt;“Show Content”&lt;/strong&gt; action.&lt;/li&gt;
&lt;li&gt;Drag it under your Shell Script action.&lt;/li&gt;
&lt;li&gt;It should automatically use the “Shell Script Result” as the alert message.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Name it something clear like &lt;strong&gt;“Image Scrub”&lt;/strong&gt;. Shortcuts auto-saves, so you can just close the window. It should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-02-04/exiftool-shortcuts.png&quot; alt=&quot;exiftool shortcuts&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 4: Test it Out&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Go to any folder in Finder.&lt;/li&gt;
&lt;li&gt;Right-click an image that you already have a backup copy of.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Quick Actions&lt;/strong&gt; &amp;gt; &lt;strong&gt;Image Scrub&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;First-time only:&lt;/strong&gt; A permission dialog will appear asking &lt;em&gt;“Finder.app” would like to access files in your Downloads folder&lt;/em&gt; (or whichever folder you’re in). Click &lt;strong&gt;Allow&lt;/strong&gt;. The script will wait for you and then proceed automatically. This only happens once per folder.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Troubleshooting: Quick Action not showing up?&lt;/strong&gt;
Sometimes on newer Macs, the Quick Actions won’t appear in the right-click menu immediately. To fix this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check your settings: Click the &lt;strong&gt;Customize…&lt;/strong&gt; button in the Quick Actions menu, and make sure the new Shortcut is checked.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relaunch Finder:&lt;/strong&gt; Hold the &lt;strong&gt;Option (⌥)&lt;/strong&gt; key, right-click the &lt;strong&gt;Finder&lt;/strong&gt; icon in your Dock, and click &lt;strong&gt;Relaunch&lt;/strong&gt;. Your new action should now appear!&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;A dialog box will appear showing you the “cleaned” metadata (which should now only show basic file system properties like file size and format, like below).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-02-04/exiftool-shortcuts-sample-result.png&quot; alt=&quot;exiftool Shortcuts sample result&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can now upload this image to the internet without any metadata that could be used to track you.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Legacy Method: Automator&lt;/h2&gt;
&lt;p&gt;If you are using an older version of macOS that does not support Shortcuts, or you don’t want to deal with the wired permission issue with Shortcuts because its sandbox protection, you can still accomplish this using the legacy &lt;strong&gt;Automator&lt;/strong&gt; app.&lt;/p&gt;
&lt;h3&gt;Step 1: Find and Open Automator&lt;/h3&gt;
&lt;p&gt;Press &lt;strong&gt;Cmd + Space&lt;/strong&gt; and type &lt;code&gt;Automator&lt;/code&gt;, then hit &lt;strong&gt;Enter&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Select &lt;strong&gt;New Document&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Choose &lt;strong&gt;Quick Action&lt;/strong&gt; as the document type and click &lt;strong&gt;Choose&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;Step 2: Configure the Inputs&lt;/h3&gt;
&lt;p&gt;At the very top of the workflow area, set the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Workflow receives current:&lt;/strong&gt; &lt;code&gt;image files&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;in:&lt;/strong&gt; &lt;code&gt;Finder&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Step 3: Add the Shell Script&lt;/h3&gt;
&lt;p&gt;In the search bar on the left, type “&lt;strong&gt;Run Shell Script&lt;/strong&gt;”.&lt;/p&gt;
&lt;p&gt;Drag that action into the main workflow area on the right.&lt;/p&gt;
&lt;p&gt;Change &lt;strong&gt;Pass input:&lt;/strong&gt; to &lt;code&gt;as arguments&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Clear out the default text and paste the following script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Include MacPorts and Homebrew (ARM/x86) paths&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; PATH&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;/opt/local/bin:/opt/homebrew/bin:/usr/local/bin:&lt;/span&gt;&lt;span&gt;$PATH&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EXIFTOOL&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;$(&lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt; -v&lt;/span&gt;&lt;span&gt; exiftool)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Check if exiftool is installed&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;-z&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$EXIFTOOL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; ]; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    osascript&lt;/span&gt;&lt;span&gt; -e&lt;/span&gt;&lt;span&gt; &apos;display alert &quot;exiftool not found&quot; message &quot;Please make sure exiftool is installed (via `brew install exiftool` or `sudo port install exiftool`).&quot;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    exit&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; f &lt;/span&gt;&lt;span&gt;in&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$@&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 1. Strip all metadata in-place&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;&lt;/span&gt;&lt;span&gt;$EXIFTOOL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; -all=&lt;/span&gt;&lt;span&gt; -overwrite_original&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # 2. Extract remaining info for the pop-up&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    echo&lt;/span&gt;&lt;span&gt; &quot;--- FILE: $(&lt;/span&gt;&lt;span&gt;basename&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;) ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;&lt;/span&gt;&lt;span&gt;$EXIFTOOL&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    echo&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;done&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;Like the Shortcuts action, setting &lt;code&gt;PATH&lt;/code&gt; explicitly ensures compatibility across Apple Silicon, Intel Macs, Homebrew, and MacPorts without manual path edits.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Step 4: Add the Display Notification&lt;/h3&gt;
&lt;p&gt;Since a shell script runs in the background, you won’t see the output unless we pipe it to a window.&lt;/p&gt;
&lt;p&gt;In the search bar on the left, type “&lt;strong&gt;Set Value of Variable&lt;/strong&gt;”. Drag it under your script.&lt;/p&gt;
&lt;p&gt;Click the “&lt;strong&gt;Variable:&lt;/strong&gt;” dropdown, select &lt;strong&gt;New Variable…&lt;/strong&gt;, and name it &lt;code&gt;metadata_output&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now, search for “&lt;strong&gt;Ask for Confirmation&lt;/strong&gt;” and drag it to the bottom.&lt;/p&gt;
&lt;p&gt;Drag the Variable named &lt;code&gt;metadata_output&lt;/code&gt; in the Message box.&lt;/p&gt;
&lt;h3&gt;Step 5: Save and Run&lt;/h3&gt;
&lt;p&gt;Now it should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-02-04/exiftool-automator.png&quot; alt=&quot;exiftool automator&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cmd + S&lt;/strong&gt; to save the file.&lt;/p&gt;
&lt;p&gt;Name it exactly what you want to see in your right-click menu, for example: &lt;code&gt;Image Scrub&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test it:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to any folder in Finder.&lt;/li&gt;
&lt;li&gt;Right-click an image that you already have a backup copy.&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Quick Actions&lt;/strong&gt; &amp;gt; &lt;strong&gt;Image Scrub&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A dialog box will appear showing you the “cleaned” metadata (which should now only show basic file system properties like file size and format like below).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-02-04/exiftool-quick-action-sample-result.png&quot; alt=&quot;exiftool quick action sample result&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can now upload this image to the internet without any concerns.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;For HEIC files: HEIC images retain some structural metadata (such as color profile and codec parameters) that ExifTool cannot remove without corrupting the file. As a result, the popup window may extend beyond your screen borders due to the volume of remaining (non-sensitive) information. If that happens, simply press &lt;strong&gt;Enter&lt;/strong&gt; or &lt;strong&gt;Esc&lt;/strong&gt; to dismiss it instead of clicking the OK button.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;A Little Bit More&lt;/h3&gt;
&lt;p&gt;You can find your saved Quick Action file that you created with &lt;strong&gt;Automator&lt;/strong&gt; at this path: &lt;code&gt;~/Library/Services/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to get there quickly:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open Finder.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Cmd + Shift + G&lt;/strong&gt; (Go to Folder).&lt;/li&gt;
&lt;li&gt;Paste &lt;code&gt;~/Library/Services/&lt;/code&gt; and hit &lt;strong&gt;Enter&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You will see a file named &lt;code&gt;*.workflow&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you ever want to change the name that appears in your right-click menu, simply rename the file inside that Services folder. To delete the Quick Action, just move that file to the Trash.&lt;/p&gt;</content:encoded></item><item><title>Private Telegram Bot for X Link Previews</title><link>https://michifumi.de/blog/2026-01-17-how-to-build-a-private-telegram-bot-to-fix-x-twitter-links-on-vps/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-01-17-how-to-build-a-private-telegram-bot-to-fix-x-twitter-links-on-vps/</guid><description>How to build, secure and deploy a private Telegram bot to automatically replace x.com links with fixupx.com for native Instant View support.</description><pubDate>Sat, 17 Jan 2026 18:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;How to build, secure and deploy a private Telegram bot to automatically replace x.com links with fixupx.com for native Instant View support.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;If you use Telegram and X a lot and often share X links in a Telegram group chat, you may have noticed that the X links cannot show as ‘Instant View’, a very convenient function natively supported by Telegram, unless you add a prefix such as ‘fixup’ before the X URL. If you hate yourself, you can manually add the prefix each time you share an X URL in the group chat. Alternatively, you can set up a Telegram bot with your VPS to add the prefix automatically. This blog contains a guide documenting the exact process followed to build, secure and deploy the private Telegram X-Link Fixer bot on a VPS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Private Telegram X-Link Fixer:&lt;/strong&gt; Automatically detect &lt;code&gt;x.com&lt;/code&gt; or &lt;code&gt;twitter.com&lt;/code&gt; links, replace them with &lt;code&gt;fixupx.com&lt;/code&gt; for better previews, remove tracking parameters, and delete the original message to keep the chat clean.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Bot Creation and Configuration (@BotFather)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create Bot:&lt;/strong&gt; Search for &lt;code&gt;@BotFather&lt;/code&gt; on Telegram and send &lt;code&gt;/newbot&lt;/code&gt;. Follow the prompts to name your bot and obtain your &lt;strong&gt;API Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disable Privacy Mode:&lt;/strong&gt; This is crucial for the bot to “see” messages containing links in group chats without needing an explicit &lt;code&gt;@mention&lt;/code&gt;:
&lt;ul&gt;
&lt;li&gt;Send &lt;code&gt;/setprivacy&lt;/code&gt; to &lt;code&gt;@BotFather&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Select your bot, then select &lt;strong&gt;Disable&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Note: If the bot is already a member of a group, remove and re-add it for this change to take effect.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permissions:&lt;/strong&gt; Add the bot to your target group chat and promote it to &lt;strong&gt;Administrator&lt;/strong&gt;. Ensure it has the &lt;strong&gt;Delete Messages&lt;/strong&gt; permission enabled so it can remove the raw link after posting the preview.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Prepare the VPS Environment&lt;/h2&gt;
&lt;p&gt;Log in to your VPS and set up a dedicated directory with a Python virtual environment to keep dependencies isolated from the system Python.&lt;/p&gt;
&lt;h3&gt;Debian and Ubuntu Setup&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Update package lists and install &lt;code&gt;python3-venv&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt&lt;/span&gt;&lt;span&gt; update&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; python3-venv&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install &lt;code&gt;pip&lt;/code&gt; using the official bootstrap script:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -sS&lt;/span&gt;&lt;span&gt; https://bootstrap.pypa.io/get-pip.py&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; python3&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create project directory and virtual environment:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/mybot&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; ~/mybot&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;python3&lt;/span&gt;&lt;span&gt; -m&lt;/span&gt;&lt;span&gt; venv&lt;/span&gt;&lt;span&gt; venv&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;source&lt;/span&gt;&lt;span&gt; venv/bin/activate&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install the required library:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pip&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; python-telegram-bot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Fedora and RHEL Setup&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Update system packages and install &lt;code&gt;python3-pip&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; dnf&lt;/span&gt;&lt;span&gt; update&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; dnf&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; python3-pip&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create project directory and virtual environment:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/mybot&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; ~/mybot&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;python3&lt;/span&gt;&lt;span&gt; -m&lt;/span&gt;&lt;span&gt; venv&lt;/span&gt;&lt;span&gt; venv&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;source&lt;/span&gt;&lt;span&gt; venv/bin/activate&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install the required library:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pip&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; python-telegram-bot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;3. The Bot Script (bot.py)&lt;/h2&gt;
&lt;p&gt;Create the script inside &lt;code&gt;~/mybot&lt;/code&gt; using &lt;code&gt;nano bot.py&lt;/code&gt;. &lt;strong&gt;Update the configuration&lt;/strong&gt; with your bot token and authorized IDs.&lt;/p&gt;
&lt;p&gt;You can use &lt;strong&gt;@userinfobot&lt;/strong&gt; in Telegram to find your user ID, group ID, and channel ID. You may also use third-party Telegram clients to retrieve your ID (such as &lt;a href=&quot;https://swiftgram.app/&quot;&gt;Swiftgram&lt;/a&gt; for Apple platforms or &lt;a href=&quot;https://f-droid.org/en/packages/org.forkgram.messenger/&quot;&gt;Forkgram&lt;/a&gt; on Android).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; re&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; telegram &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; Update&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; telegram.ext &lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; Application, MessageHandler, filters, ContextTypes&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# --- CONFIGURATION ---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;TOKEN&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &quot;YOUR_BOT_TOKEN&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Template user ID and group ID, replace with your own IDs&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Group and channel IDs must include the -100 prefix if retrieved from third-party Telegram clients&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;AUTHORIZED_IDS&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;1234567890&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;1001234567890&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Match X/Twitter links (including fixupx.com) and capture query parameters separately&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;X_PATTERN&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; r&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;https&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;://&lt;/span&gt;&lt;span&gt;(?:&lt;/span&gt;&lt;span&gt;www&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;twitter&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;fixupx&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;\s?]&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;\?&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;\s]&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;async&lt;/span&gt;&lt;span&gt; def&lt;/span&gt;&lt;span&gt; auto_fix_and_clean&lt;/span&gt;&lt;span&gt;(update: Update, context: ContextTypes.&lt;/span&gt;&lt;span&gt;DEFAULT_TYPE&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # Security: Ignore unauthorized chats&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; update.effective_chat.id &lt;/span&gt;&lt;span&gt;not&lt;/span&gt;&lt;span&gt; in&lt;/span&gt;&lt;span&gt; AUTHORIZED_IDS&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # Extract text from message or media caption&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; update.message.text &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; update.message.caption&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; not&lt;/span&gt;&lt;span&gt; text:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        return&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # Check if text contains any X/Twitter-related link&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    match &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; re.search(&lt;/span&gt;&lt;span&gt;X_PATTERN&lt;/span&gt;&lt;span&gt;, text)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; match:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        protocol, domain, path, query_params &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; match.groups()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        # Only process if URL has tracking parameters OR is not using fixupx.com&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; query_params &lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt; domain &lt;/span&gt;&lt;span&gt;!=&lt;/span&gt;&lt;span&gt; &apos;fixupx.com&apos;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            # Replace domain with fixupx.com and remove tracking parameters&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            fixed_text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; re.sub(&lt;/span&gt;&lt;span&gt;X_PATTERN&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;\1&lt;/span&gt;&lt;span&gt;fixupx&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;\3&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;, text)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            user &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; update.message.from_user.first_name&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            # 1. Send the fixed version&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            await&lt;/span&gt;&lt;span&gt; context.bot.send_message(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                chat_id&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;update.effective_chat.id,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                text&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;🛠 From &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;\n{&lt;/span&gt;&lt;span&gt;fixed_text&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            # 2. Delete original message&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                await&lt;/span&gt;&lt;span&gt; update.message.delete()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            except&lt;/span&gt;&lt;span&gt; Exception&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; e:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;Delete failed (Check Admin permissions): &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; main&lt;/span&gt;&lt;span&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    app &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Application.builder().token(&lt;/span&gt;&lt;span&gt;TOKEN&lt;/span&gt;&lt;span&gt;).build()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    app.add_handler(MessageHandler((filters.&lt;/span&gt;&lt;span&gt;TEXT&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; filters.&lt;/span&gt;&lt;span&gt;CAPTION&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;&amp;amp;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;~&lt;/span&gt;&lt;span&gt;filters.&lt;/span&gt;&lt;span&gt;COMMAND&lt;/span&gt;&lt;span&gt;), auto_fix_and_clean))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Bot is running...&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    app.run_polling()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; __name__&lt;/span&gt;&lt;span&gt; ==&lt;/span&gt;&lt;span&gt; &quot;__main__&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    main()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Optional: Match Links Only at Start of Message&lt;/h3&gt;
&lt;p&gt;By default, the bot detects and fixes X/Twitter links anywhere inside a message. If you prefer the bot to &lt;strong&gt;only&lt;/strong&gt; process links that appear at the beginning of a message, modify the regex pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Default: matches links anywhere in the message and removes tracking parameters&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;X_PATTERN&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; r&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;https&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;://&lt;/span&gt;&lt;span&gt;(?:&lt;/span&gt;&lt;span&gt;www&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;twitter&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;fixupx&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;\s?]&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;\?&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;\s]&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Alternative: matches links only at the start of the message and removes tracking parameters&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;X_PATTERN&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; r&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;span&gt;^(&lt;/span&gt;&lt;span&gt;https&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;://&lt;/span&gt;&lt;span&gt;(?:&lt;/span&gt;&lt;span&gt;www&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;x&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;twitter&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;fixupx&lt;/span&gt;&lt;span&gt;\.&lt;/span&gt;&lt;span&gt;com&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;\s?]&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;)(&lt;/span&gt;&lt;span&gt;\?&lt;/span&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;\s]&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;^&lt;/code&gt; anchor ensures the pattern triggers only when the message begins with the URL, preventing casual references within longer sentences from being captured.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;The bot handles all X-related URLs intelligently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Converts &lt;code&gt;x.com&lt;/code&gt; and &lt;code&gt;twitter.com&lt;/code&gt; to &lt;code&gt;fixupx.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Strips tracking queries (such as &lt;code&gt;?s=46&amp;amp;t=xxx&lt;/code&gt;) from all links, including existing &lt;code&gt;fixupx.com&lt;/code&gt; shares&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://x.com/user/status/123?s=46&amp;amp;t=abc&lt;/code&gt; → &lt;code&gt;https://fixupx.com/user/status/123&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://fixupx.com/user/status/123?s=46&amp;amp;t=abc&lt;/code&gt; → &lt;code&gt;https://fixupx.com/user/status/123&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://fixupx.com/user/status/123&lt;/code&gt; → No action (already clean)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;4. Deploy as a Systemd Background Service&lt;/h2&gt;
&lt;p&gt;To ensure the bot continues running after closing your SSH session and restarts on server reboots, configure a &lt;code&gt;systemd&lt;/code&gt; unit.&lt;/p&gt;
&lt;h3&gt;Service File Configuration&lt;/h3&gt;
&lt;p&gt;Create the service file using &lt;code&gt;sudo nano /etc/systemd/system/tgbot.service&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;[Unit]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Description&lt;/span&gt;&lt;span&gt;=Telegram X-Link Fixer Bot&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;After&lt;/span&gt;&lt;span&gt;=network.target&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Service]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;User&lt;/span&gt;&lt;span&gt;=linuxuser&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Group&lt;/span&gt;&lt;span&gt;=linuxuser&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;WorkingDirectory&lt;/span&gt;&lt;span&gt;=/home/linuxuser/mybot&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ExecStart&lt;/span&gt;&lt;span&gt;=/home/linuxuser/mybot/venv/bin/python bot.py&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Restart&lt;/span&gt;&lt;span&gt;=always&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Install]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;WantedBy&lt;/span&gt;&lt;span&gt;=multi-user.target&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;Replace &lt;code&gt;linuxuser&lt;/code&gt; and &lt;code&gt;/home/linuxuser/mybot&lt;/code&gt; with your actual Linux user and project directory path.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Enable and Start the Service&lt;/h3&gt;
&lt;p&gt;Reload the systemd daemon, enable automatic startup on boot, and start the service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; daemon-reload&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; enable&lt;/span&gt;&lt;span&gt; --now&lt;/span&gt;&lt;span&gt; tgbot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Troubleshooting and Monitoring&lt;/h2&gt;
&lt;p&gt;Use these standard &lt;code&gt;systemd&lt;/code&gt; and &lt;code&gt;journalctl&lt;/code&gt; commands to inspect and manage your running bot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Service Status:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt; tgbot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;View Live Logs in Real Time:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; journalctl&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; tgbot.service&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Restart After Script Changes:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; tgbot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>The best Safari extension: uBlock Origin Lite</title><link>https://michifumi.de/blog/2026-01-17-the-best-safari-extension-ublock-origin-lite/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-01-17-the-best-safari-extension-ublock-origin-lite/</guid><description>Let&apos;s get rid of YouTube Shorts!</description><pubDate>Sat, 17 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Let&apos;s get rid of YouTube Shorts!&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;The &lt;a href=&quot;https://ublockorigin.com/&quot;&gt;uBlock Origin Lite&lt;/a&gt; was released on the App Store in August 2025. Although it’s the “Lite” version, which is not as powerful as the original, it’s still one of the best things to happen in the last year, especially for users who sometimes have to use Safari as their browser, like me.&lt;/p&gt;
&lt;p&gt;The best options for normal users who want to protect their privacy are still to use Firefox with the &lt;a href=&quot;https://github.com/arkenfox/user.js/&quot;&gt;user.js file&lt;/a&gt; for hardening or the more user-friendly &lt;a href=&quot;https://brave.com/&quot;&gt;Brave browser&lt;/a&gt;. However, Safari sometimes has its own advantages, such as the battery saver. To be honest, “thanks” to its system-native status, it conserves battery power far more effectively than other third-party browsers. But this remains its sole redeeming feature. In terms of web compatibility, privacy, and extensibility, Safari undoubtedly ranks at the bottom.&lt;/p&gt;
&lt;p&gt;Until the arrival of uBlock Origin Lite.&lt;/p&gt;
&lt;p&gt;Those familiar with it will have long heard its reputation, while if you’re encountering it for the first time, all you need to know for now is that it can magically eliminate almost all manner of irritating adverts and pop-ups, including but not limited to YouTube, X, Instagram, Facebook (yes, you won’t need to pay a penny for a subscription!), and numerous other websites.&lt;/p&gt;
&lt;p&gt;Moreover, if you couldn’t care less about short videos but find yourself constantly bombarded by sites like YouTube, which seem determined to use every trick in the book to push short-form content on you, then uBlock Origin Lite is absolutely the solution for you.&lt;/p&gt;
&lt;p&gt;It works on all Apple ecosystems, but this blog focuses on how you can set it up step by step on macOS. The process is similar for other systems, and I’m sure you can learn by analogy.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Here is how this magic is achieved&lt;/h2&gt;
&lt;p&gt;First, download and install it from the &lt;a href=&quot;https://apps.apple.com/us/app/ublock-origin-lite/id6745342698&quot;&gt;App Store&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-01-17/ublock-origin-lite.png&quot; alt=&quot;uBlock Origin Lite App Store page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Then enable it in Safari’s extension settings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-01-17/ubol-safari-extension-settings.png&quot; alt=&quot;uBlock Origin Lite Safari Extension Settings&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Open the uBlock Origin Lite dashboard by clicking the icon in the toolbar and select the native rules from the &lt;strong&gt;Filter lists&lt;/strong&gt;. If you want, you can also select your regions and languages using the filter at the bottom of the lists, but the below setting is good enough for most senarios.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-01-17/ubol-native-filter-lists.png&quot; alt=&quot;uBlock Origin Lite Native Filter Lists&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Now for the important part&lt;/h2&gt;
&lt;p&gt;By following the steps above, we’ve managed to get rid of ads and most of the annoying pop-ups. But if you absolutely hate short videos like me, we’ll need to take a few extra steps.&lt;/p&gt;
&lt;p&gt;First, copy these rules by click the copy button below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;! YT - Homepage and Subscriptions (Grid View) - Hide the Shorts section&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;youtube.com##[is-shorts]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - Menu - Hide the Shorts button&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com###guide [title=&quot;Shorts&quot;], .ytd-mini-guide-entry-renderer[title=&quot;Shorts&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - Search - Hide Shorts&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com##ytd-search ytd-video-renderer:has([overlay-style=&quot;SHORTS&quot;],[href^=&quot;/shorts/&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - Search, Channels, Subscriptions (List View) and Sidebar/Below Player Recommendations - Hide the Shorts sections&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com##ytd-reel-shelf-renderer&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - Channels - Hide the Shorts tab&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com##[tab-title=&quot;Shorts&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - Subscriptions - Hide Shorts - Grid View&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com##ytd-browse[page-subtype=&quot;subscriptions&quot;] ytd-grid-video-renderer:has([overlay-style=&quot;SHORTS&quot;],[href^=&quot;/shorts/&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - Subscriptions - Hide Shorts - List View&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com##ytd-browse[page-subtype=&quot;subscriptions&quot;] ytd-video-renderer:has([overlay-style=&quot;SHORTS&quot;],[href^=&quot;/shorts/&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - Subscriptions - New Layout - Hide Shorts&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com##ytd-browse[page-subtype=&quot;subscriptions&quot;] ytd-rich-item-renderer:has([overlay-style=&quot;SHORTS&quot;],[href^=&quot;/shorts/&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - Sidebar - Hide Shorts&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com###related :is(ytd-compact-video-renderer,yt-lockup-view-model):has([overlay-style=&quot;SHORTS&quot;],[href^=&quot;/shorts/&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT - History - Hide Shorts&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;www.youtube.com##ytd-item-section-renderer[page-subtype]:has(&amp;gt;#contents&amp;gt;[is-history]&amp;gt;#dismissible&amp;gt;ytd-thumbnail&amp;gt;#thumbnail[href^=&quot;/shorts/&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT Mobile - Hide the Shorts Menu button&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;m.youtube.com##ytm-pivot-bar-item-renderer:has(&amp;gt;.pivot-shorts)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT Mobile - Hide the Shorts sections&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;m.youtube.com##ytm-reel-shelf-renderer&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;m.youtube.com##ytm-rich-section-renderer:has([d^=&quot;M17.77,10.32l-1.2&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT Mobile - Search - Hide Shorts&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;m.youtube.com##ytm-search ytm-video-with-context-renderer:has([data-style=&quot;SHORTS&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT Mobile - Channels - Hide the Shorts button&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;m.youtube.com##[tab-title=&quot;Shorts&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT Mobile - History - Hide Shorts&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;m.youtube.com##[tab-identifier=&quot;FEhistory&quot;] ytm-compact-video-renderer:has(&amp;gt;div&amp;gt;a[href^=&quot;/shorts/&quot;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT Search - keep only videos (no shorts)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;youtube.com##ytd-search ytd-item-section-renderer&amp;gt;#contents&amp;gt;:is(:not(ytd-video-renderer,yt-showing-results-for-renderer,[icon-name=&quot;promo-full-height:EMPTY_SEARCH&quot;]),ytd-video-renderer:has([aria-label=&quot;Shorts&quot;])),ytd-secondary-search-container-renderer&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT Search - keep only videos (no shorts) and channels&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;youtube.com##ytd-search ytd-item-section-renderer&amp;gt;#contents&amp;gt;:is(:not(ytd-video-renderer,ytd-channel-renderer,yt-showing-results-for-renderer,[icon-name=&quot;promo-full-height:EMPTY_SEARCH&quot;]),ytd-video-renderer:has([aria-label=&quot;Shorts&quot;])),ytd-secondary-search-container-renderer&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;! YT Search - keep only videos (no shorts), channels and playlists&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;youtube.com##ytd-search ytd-item-section-renderer&amp;gt;#contents&amp;gt;:is(:not(ytd-video-renderer,ytd-channel-renderer,ytd-playlist-renderer,yt-lockup-view-model,yt-showing-results-for-renderer,[icon-name=&quot;promo-full-height:EMPTY_SEARCH&quot;]),ytd-video-renderer:has([aria-label=&quot;Shorts&quot;])),ytd-secondary-search-container-renderer&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;These rules may change from time to time, so it’s a good idea to check the &lt;a href=&quot;https://www.reddit.com/r/uBlockOrigin/wiki/solutions/youtube/#wiki_shorts&quot;&gt;official wiki page&lt;/a&gt; for the latest updates.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then, in the dashboard, go to &lt;strong&gt;Custom filters &amp;gt; Import / Export&lt;/strong&gt;, paste what you just copied and click &lt;strong&gt;✓Add&lt;/strong&gt; button. It should look similar to this after clicking:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-01-17/ubol-custom-filter-lists.png&quot; alt=&quot;uBlock Origin Lite Custom Filter Lists&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Then you need to quit Safari completely by pressing &lt;strong&gt;cmd + Q&lt;/strong&gt;. This is &lt;strong&gt;essential&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Now reopen Safari, open YouTube, and savour your triumph! Those pesky adverts and short video pushes have vanished both in homepage and in search results!&lt;/p&gt;
&lt;p&gt;Keep it in mind, this is rather like a game of cat and mouse, so perhaps one day this extension may cease to function (but it is not this day! This day we fight!). Should you wish to contribute, when encountering usage issues, you may visit &lt;a href=&quot;https://github.com/uBlockOrigin/uAssets/issues/30158&quot;&gt;this GitHub issue&lt;/a&gt; to help the developers identify the problem more quickly.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Update&lt;/h2&gt;
&lt;p&gt;If you use Safari every day (god I hate to say that), you may have noticed that, each time the UBOL updates, its functions stop working, even if you restart Safari completely. In order to solve this problem, you need to complete the following steps.&lt;/p&gt;
&lt;p&gt;Firstly, according to the official update logs, you need to wait a few seconds before it is fully ready. The more rules you set, the longer you will need to wait for them to take effect. I usually wait for around &lt;strong&gt;10&lt;/strong&gt; seconds to &lt;strong&gt;30&lt;/strong&gt; seconds after opening a new YouTube page, depending which device I’m using.&lt;/p&gt;
&lt;p&gt;Secondly, if you feel you have waited long enough after opening a YouTube page, find the extension icon in the Safari Toolbar. Click it and change the filtering mode from the default &lt;code&gt;Optimal&lt;/code&gt; to &lt;code&gt;Complete&lt;/code&gt; (or any other option).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-01-17/ubol-settings.png&quot; alt=&quot;UBOL Settings&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Then change it back to &lt;code&gt;Optimal&lt;/code&gt;. The webpage will refresh automatically and the ads and shorts will have disappeared!&lt;/p&gt;</content:encoded></item><item><title>Daily Updates on Linux and macOS</title><link>https://michifumi.de/blog/2026-01-07-mastering-the-art-of-daily-updates-on-linux/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-01-07-mastering-the-art-of-daily-updates-on-linux/</guid><description>Satisfying the compulsion for daily system updates with a custom bash script for Linux and Docker.</description><pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Satisfying the compulsion for daily system updates with a custom bash script for Linux and Docker.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;There are two types of people: those who never update their software and those who do it every day.&lt;/p&gt;
&lt;p&gt;Clearly, I’m the latter. I even perform various update operations daily across different devices: from system-level to application-level. And whenever I’m feeling bored, “update” is absolutely my default unconscious choice. I’m so addicted to updating that I wonder if it’s some kind of disease such as compulsive disorder. Perhaps medical or psychological experts have already conducted similar research on this?&lt;/p&gt;
&lt;p&gt;Anyway, while updating functions on popular operating systems like iOS, macOS, Android, and Windows has become remarkably straightforward and user-friendly, the process remains less intuitive for Linux systems, particularly when using the command-line interface (CLI). Although users can leverage unattended-upgrades for automated updates, this solution doesn’t address all challenges for heavy Docker users.&lt;/p&gt;
&lt;p&gt;That’s why I need to write an update script to satisfy my perverted desire to update.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Installation and Usage&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create the script file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; nano&lt;/span&gt;&lt;span&gt; /usr/local/bin/up.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Paste the content from your OS-specific script (&lt;a href=&quot;#debian-based-linux&quot;&gt;Debian-based Linux&lt;/a&gt;, &lt;a href=&quot;#fedorarhelcentos-8&quot;&gt;Fedora/RHEL/CentOS 8+&lt;/a&gt;, or &lt;a href=&quot;#fedora-coreos-automatic-updates-via-zincati&quot;&gt;Fedora CoreOS&lt;/a&gt;) into the file and save it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Make the script executable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; chmod&lt;/span&gt;&lt;span&gt; +x&lt;/span&gt;&lt;span&gt; /usr/local/bin/up.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run the script from anywhere in the terminal:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;up.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Debian-based Linux&lt;/h2&gt;
&lt;p&gt;Here is the &lt;code&gt;up.sh&lt;/code&gt; script that handles system and Docker updates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Ensure the script exits if any command fails&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; -e&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Starting System Update ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt-get&lt;/span&gt;&lt;span&gt; update&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt-get&lt;/span&gt;&lt;span&gt; full-upgrade&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt-get&lt;/span&gt;&lt;span&gt; autoremove&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Vacuuming Systemd Logs ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; journalctl&lt;/span&gt;&lt;span&gt; --vacuum-size=200M&lt;/span&gt;&lt;span&gt; --vacuum-time=14d&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Updating Docker Containers via Watchtower ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Note: Docker API version is essential for Watchtower to function correctly&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt; --rm&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    -e&lt;/span&gt;&lt;span&gt; DOCKER_API_VERSION=&lt;/span&gt;&lt;span&gt;1.44&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    -v&lt;/span&gt;&lt;span&gt; /var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    containrrr/watchtower&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    --run-once&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    --cleanup&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    your&lt;/span&gt;&lt;span&gt; container&lt;/span&gt;&lt;span&gt; names&lt;/span&gt;&lt;span&gt; (separate &lt;/span&gt;&lt;span&gt;by&lt;/span&gt;&lt;span&gt; space&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Cleaning up unused Docker resources ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; container&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Remove -a to keep cached images or keep it for deep clean&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; image&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -a&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; volume&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; network&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; builder&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;Update complete!&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;#installation-and-usage&quot;&gt;Go back Step 3 to continue&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Fedora/RHEL/CentOS 8+&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; -e&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Starting System Update ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; dnf&lt;/span&gt;&lt;span&gt; upgrade&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; dnf&lt;/span&gt;&lt;span&gt; autoremove&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Vacuuming Systemd Logs ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; journalctl&lt;/span&gt;&lt;span&gt; --vacuum-size=200M&lt;/span&gt;&lt;span&gt; --vacuum-time=14d&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Updating Docker Containers via Watchtower ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Note: Docker API version is essential for Watchtower to function correctly&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt; --rm&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    -e&lt;/span&gt;&lt;span&gt; DOCKER_API_VERSION=&lt;/span&gt;&lt;span&gt;1.44&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    -v&lt;/span&gt;&lt;span&gt; /var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    containrrr/watchtower&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    --run-once&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    --cleanup&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    your&lt;/span&gt;&lt;span&gt; container&lt;/span&gt;&lt;span&gt; names&lt;/span&gt;&lt;span&gt; (separate &lt;/span&gt;&lt;span&gt;by&lt;/span&gt;&lt;span&gt; space&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Cleaning up unused Docker resources ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; container&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Remove -a to keep cached images or keep it for deep clean&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; image&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -a&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; volume&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; network&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; docker&lt;/span&gt;&lt;span&gt; builder&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;Update complete!&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;#installation-and-usage&quot;&gt;Go back Step 3 to continue&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Fedora CoreOS (Automatic Updates via Zincati)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Fedora CoreOS is different&lt;/strong&gt;, it uses &lt;strong&gt;Zincati&lt;/strong&gt; for fully automatic updates. Unlike traditional Linux distributions, Fedora CoreOS requires &lt;strong&gt;no manual intervention&lt;/strong&gt; for system updates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How Zincati Works:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Zincati is the automatic update agent that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Periodically checks&lt;/strong&gt; for new Fedora CoreOS releases from the Cincinnati update server&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatically downloads and stages&lt;/strong&gt; updates in the background (no bandwidth concerns during work hours)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handles reboots&lt;/strong&gt; based on your configured strategy&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Update Strategies:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Zincati supports different reboot strategies (configured in &lt;code&gt;/etc/zincati/config.d/&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;immediate&lt;/code&gt;&lt;/strong&gt;: Reboots immediately after staging an update (default on Google Cloud Platform)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fleet_lock&lt;/code&gt;&lt;/strong&gt;: Coordinates with other nodes to prevent simultaneous reboots (cluster-friendly)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;periodic&lt;/code&gt;&lt;/strong&gt;: Reboots only during specified time windows (e.g., 2-4 AM on weekends)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can check your current strategy with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt; zincati&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; grep&lt;/span&gt;&lt;span&gt; &quot;update strategy&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Fedora CoreOS uses &lt;code&gt;rpm-ostree&lt;/code&gt;, which means updates are &lt;strong&gt;atomic&lt;/strong&gt; and &lt;strong&gt;always require a reboot&lt;/strong&gt;. The new version is staged as a separate deployment and only becomes active after reboot. This ensures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zero chance of broken updates (either fully applied or not at all)&lt;/li&gt;
&lt;li&gt;Instant rollback capability (previous version remains available)&lt;/li&gt;
&lt;li&gt;No “partially updated” system states&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Script:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Since Zincati handles system updates automatically, the script focuses on &lt;strong&gt;checking status&lt;/strong&gt; and &lt;strong&gt;updating containers&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt; -euo&lt;/span&gt;&lt;span&gt; pipefail&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Checking for staged system updates ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;rpm-ostree&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Update toolbox container&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# After a major version upgrade, the default toolbox&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# container won&apos;t exist yet. Create it automatically if missing.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Updating Toolbox Container ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; !&lt;/span&gt;&lt;span&gt; toolbox&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    echo&lt;/span&gt;&lt;span&gt; &quot;Default toolbox container not found. Creating...&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    toolbox&lt;/span&gt;&lt;span&gt; create&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;fi&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Upgrade, autoremove dependencies, AND purge the massive DNF download cache&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;toolbox&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt; sudo&lt;/span&gt;&lt;span&gt; dnf&lt;/span&gt;&lt;span&gt; upgrade&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;toolbox&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt; sudo&lt;/span&gt;&lt;span&gt; dnf&lt;/span&gt;&lt;span&gt; autoremove&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;toolbox&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt; sudo&lt;/span&gt;&lt;span&gt; dnf&lt;/span&gt;&lt;span&gt; clean&lt;/span&gt;&lt;span&gt; all&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Vacuuming Systemd Logs ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; journalctl&lt;/span&gt;&lt;span&gt; --vacuum-size=200M&lt;/span&gt;&lt;span&gt; --vacuum-time=14d&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Clean up unused Podman resources&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;--- Cleaning up unused Podman resources ---&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Deletes stopped containers, unused networks, and dangling images&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;podman&lt;/span&gt;&lt;span&gt; system&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Removes unused volumes specifically (since system prune doesn&apos;t include volumes by default)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;podman&lt;/span&gt;&lt;span&gt; volume&lt;/span&gt;&lt;span&gt; prune&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Optional deep clean: Removes all unused images, not just dangling ones&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# podman image prune -a -f&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;Update complete!&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;Note: System updates are managed automatically by Zincati.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Fedora CoreOS uses Podman by default. For automatic container updates, use &lt;a href=&quot;https://docs.podman.io/en/stable/markdown/podman-auto-update.1.html&quot;&gt;&lt;code&gt;podman auto-update&lt;/code&gt;&lt;/a&gt; with containers running inside systemd units and the &lt;code&gt;io.containers.autoupdate&lt;/code&gt; label. Podman also ships with a &lt;code&gt;podman-auto-update.timer&lt;/code&gt; that triggers updates daily.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cleaning up old toolbox containers after a major version upgrade:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After Zincati upgrades Fedora CoreOS to a new major version, the script above creates a new toolbox container. The old toolbox container will remain on disk. To check and clean it up:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;toolbox&lt;/span&gt;&lt;span&gt; list&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll see something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;CONTAINER ID  CONTAINER NAME      CREATED      STATUS   IMAGE NAME&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;a1b2c3d4e5f6  fedora-toolbox-43   3 months ago  running  registry.fedoraproject.org/fedora-toolbox:43&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;f6e5d4c3b2a1  fedora-toolbox-44   2 minutes ago running  registry.fedoraproject.org/fedora-toolbox:44&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your files in &lt;code&gt;~/&lt;/code&gt; (e.g. &lt;code&gt;~/backups&lt;/code&gt;, &lt;code&gt;~/page-stats&lt;/code&gt;) are &lt;strong&gt;not&lt;/strong&gt; stored inside the toolbox — toolbox bind-mounts your home directory into the container. Removing an old toolbox only deletes the &lt;strong&gt;packages&lt;/strong&gt; installed via &lt;code&gt;dnf&lt;/code&gt; inside it (Node.js, npm, etc.), not your files.&lt;/p&gt;
&lt;p&gt;If you no longer need the old toolbox:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Stop and remove the old container&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;podman&lt;/span&gt;&lt;span&gt; stop&lt;/span&gt;&lt;span&gt; fedora-toolbox-43&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;toolbox&lt;/span&gt;&lt;span&gt; rm&lt;/span&gt;&lt;span&gt; fedora-toolbox-43&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Optionally, remove the old base image to free disk space&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;podman&lt;/span&gt;&lt;span&gt; rmi&lt;/span&gt;&lt;span&gt; registry.fedoraproject.org/fedora-toolbox:43&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;#installation-and-usage&quot;&gt;Go back Step 3 to continue&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Bonus: Daily Updates on macOS (Homebrew)&lt;/h2&gt;
&lt;p&gt;If you are on macOS and want to satisfy your update craving with Homebrew, this single command is all you need for aggressive maintenance:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; upgrade&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; autoremove&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; cleanup&lt;/span&gt;&lt;span&gt; --prune=all&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;brew upgrade&lt;/strong&gt;: Upgrades all packages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;brew autoremove&lt;/strong&gt;: Removes orphan dependencies that are no longer needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;brew cleanup —prune=all&lt;/strong&gt;: Aggressively clears the cache to free up maximum disk space, I recommend using it with the 256 GB SSD MacBook Air (that is something only Apple can do).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also don’t forget to disable the analysis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;brew&lt;/span&gt;&lt;span&gt; analytics&lt;/span&gt;&lt;span&gt; off&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make this even easier, add an alias for the terminal &lt;code&gt;~/.zshrc&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;nano&lt;/span&gt;&lt;span&gt; ~/.zshrc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;alias&lt;/span&gt;&lt;span&gt; up&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&apos;brew upgrade &amp;amp;&amp;amp; brew autoremove &amp;amp;&amp;amp; brew cleanup --prune=all&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then reload your shell:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;source&lt;/span&gt;&lt;span&gt; ~/.zshrc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can simply type &lt;code&gt;up&lt;/code&gt; to update everything that installed from homebrew!&lt;/p&gt;</content:encoded></item><item><title>Ditch Passwords: Secure Your VPS with SSH Keys</title><link>https://michifumi.de/blog/2026-01-01-ditch-passwords-secure-your-vps-with-ssh-keys/</link><guid isPermaLink="true">https://michifumi.de/blog/2026-01-01-ditch-passwords-secure-your-vps-with-ssh-keys/</guid><description>We move beyond complex passwords and harden SSH access using ed25519 keys, proper permissions, and cloud-init overrides.</description><pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;We move beyond complex passwords and harden SSH access using ed25519 keys, proper permissions, and cloud-init overrides.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;Last time we covered how to enhance VPS security by creating regular users and changing passwords to more complex ones. Here’s an image for reference:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2026-01-01/how-safe-is-your-password.jpeg&quot; alt=&quot;How safe is your password&quot; /&gt;&lt;/p&gt;
&lt;p&gt;But is a password the most secure option? Not necessarily. Because even if your password is long and complex, password-based SSH logins still suffer from a few fundamental problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;They can be brute-forced&lt;/strong&gt; (even if it’s unlikely, attackers will try constantly).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They rely on something you type&lt;/strong&gt;, which means they can be phished, logged, or reused.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They expand the attack surface&lt;/strong&gt;, you’re leaving a whole authentication method enabled on the server.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The better approach is:&lt;br /&gt;
🔐 &lt;strong&gt;Use SSH keys&lt;/strong&gt; and disable password logins completely.&lt;/p&gt;
&lt;p&gt;This article documents the full journey: generating a strong key pair on macOS (or on whatever system you want), installing the key correctly on the server, fixing permission issues, and (most importantly) handling the tricky part, &lt;strong&gt;cloud-init overriding your SSH config&lt;/strong&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Why SSH Keys are Better than Passwords&lt;/h2&gt;
&lt;p&gt;SSH keys are essentially cryptographic credentials. Instead of “something you know” (password), SSH keys rely on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;private key&lt;/strong&gt; (kept on your machine)&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;public key&lt;/strong&gt; (stored on your server)&lt;/li&gt;
&lt;li&gt;optional &lt;strong&gt;passphrase&lt;/strong&gt; protection for the private key&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Benefits&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Extremely resistant to brute force&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No password guessing over the network&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can lock down access&lt;/strong&gt; to key-only authentication&lt;/li&gt;
&lt;li&gt;You can manage multiple keys and revoke them cleanly&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 1: Generate an ed25519 SSH Key&lt;/h2&gt;
&lt;p&gt;We used this command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ssh-keygen&lt;/span&gt;&lt;span&gt; -t&lt;/span&gt;&lt;span&gt; ed25519&lt;/span&gt;&lt;span&gt; -a&lt;/span&gt;&lt;span&gt; 100&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;What does &lt;code&gt;-a 100&lt;/code&gt; mean?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;-a&lt;/code&gt; controls the number of key derivation rounds used to protect your private key when you set a passphrase.&lt;/p&gt;
&lt;p&gt;More rounds = harder to brute-force the passphrase if your private key is stolen.&lt;/p&gt;
&lt;h3&gt;Where does the key go?&lt;/h3&gt;
&lt;p&gt;On macOS, if you press Enter when asked for a file path, the key will be stored in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt; (private key)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt; (public key)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can use &lt;code&gt;-f&lt;/code&gt; to specify a file path.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 2: Should You Use a Passphrase?&lt;/h2&gt;
&lt;p&gt;During the generation process, you will be asked if you want to set up a passphrase for your key. In most situations, the answer is&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Yes.&lt;/strong&gt; Highly recommended.&lt;/p&gt;
&lt;p&gt;If your laptop is compromised and your private key is stolen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;without passphrase: attacker gets instant access&lt;/li&gt;
&lt;li&gt;with passphrase: attacker must brute-force it offline (very expensive)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If convenience is a concern, macOS can store the passphrase via Keychain so you don’t type it every time.&lt;/p&gt;
&lt;p&gt;For the simplest way, you can also leave it empty.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3: Put the Public Key on the VPS (Correct Location)&lt;/h2&gt;
&lt;p&gt;This is where many people get confused (including me).&lt;/p&gt;
&lt;h3&gt;Important concept&lt;/h3&gt;
&lt;p&gt;Your public key file name on your Mac can be anything, like &lt;code&gt;id_ed25519.pub&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But on an Ubuntu VPS, SSH expects keys to be listed inside:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;~&lt;/span&gt;&lt;span&gt;/.ssh/authorized_keys&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That means for user &lt;code&gt;ubuntu&lt;/code&gt;, the path is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/home/ubuntu/.ssh/authorized_keys&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use this if there’s no &lt;code&gt;.ssh/&lt;/code&gt; folder&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/.ssh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simply placing the &lt;code&gt;id_ed25519.pub&lt;/code&gt; file in the &lt;code&gt;.ssh/&lt;/code&gt; folder will not work.&lt;/p&gt;
&lt;p&gt;According to the &lt;a href=&quot;https://help.ubuntu.com/community/SSH/OpenSSH/Keys&quot;&gt;official documentation&lt;/a&gt;,&lt;/p&gt;
&lt;h3&gt;Option A (recommended): &lt;code&gt;ssh-copy-id&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ssh-copy-id&lt;/span&gt;&lt;span&gt; ubuntu@your-vps&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Option B: manual copy&lt;/h3&gt;
&lt;p&gt;On your local machine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; ~/.ssh/id_ed25519.pub&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the output, then on the VPS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/.ssh&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;nano&lt;/span&gt;&lt;span&gt; ~/.ssh/authorized_keys&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Paste the key on a new line.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 4: Fix Permissions (Why &lt;code&gt;chmod 700&lt;/code&gt; and &lt;code&gt;chmod 600&lt;/code&gt;?)&lt;/h2&gt;
&lt;p&gt;SSH is strict about file permissions for good reason.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;.ssh&lt;/code&gt; or &lt;code&gt;authorized_keys&lt;/code&gt; is accessible or writable by others, SSH assumes it could be tampered with and may ignore it.&lt;/p&gt;
&lt;p&gt;Run these on the VPS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;chmod&lt;/span&gt;&lt;span&gt; 700&lt;/span&gt;&lt;span&gt; ~/.ssh&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;chmod&lt;/span&gt;&lt;span&gt; 600&lt;/span&gt;&lt;span&gt; ~/.ssh/authorized_keys&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;chown&lt;/span&gt;&lt;span&gt; -R&lt;/span&gt;&lt;span&gt; $USER&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;$USER &lt;/span&gt;&lt;span&gt;~/.ssh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If above cmd show no output: that’s normal. Silent success is expected.&lt;/p&gt;
&lt;p&gt;To verify:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt; -ld&lt;/span&gt;&lt;span&gt; ~/.ssh&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ls&lt;/span&gt;&lt;span&gt; -l&lt;/span&gt;&lt;span&gt; ~/.ssh/authorized_keys&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;drwx------ 2 ubuntu ubuntu ... .ssh&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;-rw------- 1 ubuntu ubuntu ... authorized_keys&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 5: Confirm Key Login Works First&lt;/h2&gt;
&lt;p&gt;Before disabling passwords, &lt;strong&gt;always test key login in a new terminal&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ssh&lt;/span&gt;&lt;span&gt; ubuntu@your-vps&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it logs in without asking for the VPS password (it may ask your key passphrase), you’re ready.&lt;/p&gt;
&lt;p&gt;Keep your current SSH session open during testing so you don’t lock yourself out.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 6: Disable Password Login (And It Didn’t Work at First)&lt;/h2&gt;
&lt;p&gt;After editing &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and adding:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;PasswordAuthentication no&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PermitRootLogin no&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PubkeyAuthentication yes&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;KbdInteractiveAuthentication no&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I restarted SSH and even restarted &lt;code&gt;ssh.socket&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But password login still worked.&lt;/p&gt;
&lt;h3&gt;The truth came from&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; sshd&lt;/span&gt;&lt;span&gt; -T&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; grep&lt;/span&gt;&lt;span&gt; passwordauthentication&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It showed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;passwordauthentication yes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even though my config file clearly said &lt;code&gt;no&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So what happened?&lt;/p&gt;
&lt;h3&gt;The Real Issue: RackNerd &lt;code&gt;50-cloud-init.conf&lt;/code&gt; Overriding Your Config&lt;/h3&gt;
&lt;p&gt;On RackNerd Ubuntu VPS templates, the SSH config often includes a directive at line 1 without a &lt;code&gt;#&lt;/code&gt; comment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Include /etc/ssh/sshd_config.d/*.conf&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And inside that folder I found:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;/etc/ssh/sshd_config.d/50-cloud-init.conf&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That file had:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;PasswordAuthentication yes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the catch with OpenSSH: &lt;strong&gt;for each keyword, the first obtained value wins&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;Include /etc/ssh/sshd_config.d/*.conf&lt;/code&gt; is at line 1 of Ubuntu’s default &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;, OpenSSH parsed &lt;code&gt;50-cloud-init.conf&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; reading the rest of &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;. Since it encountered &lt;code&gt;PasswordAuthentication yes&lt;/code&gt; first, it completely ignored the &lt;code&gt;no&lt;/code&gt; in the main config!&lt;/p&gt;
&lt;p&gt;Fix: change it to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;PasswordAuthentication no&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then restart SSH (e.g., &lt;code&gt;sudo systemctl restart ssh&lt;/code&gt; on Ubuntu 22.04, or &lt;code&gt;sudo systemctl restart ssh.socket&lt;/code&gt; on Ubuntu 24.04).&lt;/p&gt;
&lt;p&gt;After that, password login stopped working.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 7: Make It Future-Proof (Recommended)&lt;/h2&gt;
&lt;p&gt;Cloud-init can regenerate or overwrite &lt;code&gt;50-cloud-init.conf&lt;/code&gt; during system updates or re-provisioning.&lt;/p&gt;
&lt;p&gt;Because OpenSSH uses &lt;strong&gt;first match wins&lt;/strong&gt; and loads drop-in files in alphabetical order, we want our custom configuration to load &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;50-cloud-init.conf&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We do this by creating a drop-in file with a lower number prefix like &lt;code&gt;01-&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; tee&lt;/span&gt;&lt;span&gt; /etc/ssh/sshd_config.d/01-hardening.conf&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;EOF&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PasswordAuthentication no&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;KbdInteractiveAuthentication no&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PermitRootLogin no&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PubkeyAuthentication yes&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;code&gt;01-hardening.conf&lt;/code&gt; is loaded alphabetically before &lt;code&gt;50-cloud-init.conf&lt;/code&gt;, its rules take priority even if cloud-init resets &lt;code&gt;50-cloud-init.conf&lt;/code&gt; in the future.&lt;/p&gt;
&lt;h3&gt;Test syntax before restarting&lt;/h3&gt;
&lt;p&gt;Before restarting the SSH service, &lt;strong&gt;always test your configuration syntax first&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; sshd&lt;/span&gt;&lt;span&gt; -t&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No output&lt;/strong&gt;: That’s good! Silent success means the configuration has no syntax errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Restart SSH service&lt;/h3&gt;
&lt;p&gt;Once verified, apply the changes by restarting the SSH service. The command depends on your distribution version:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt; (uses systemd socket activation by default):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; ssh.socket&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ubuntu 22.04 LTS / Debian / older systems&lt;/strong&gt; (uses classic standalone service):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; ssh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;If you’re unsure which init mode your server uses, you can run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; ssh&lt;/span&gt;&lt;span&gt; 2&amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; ssh.socket&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 8: Verify Password Login Is Truly Disabled&lt;/h2&gt;
&lt;p&gt;On the VPS, check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; sshd&lt;/span&gt;&lt;span&gt; -T&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; grep&lt;/span&gt;&lt;span&gt; -E&lt;/span&gt;&lt;span&gt; &apos;passwordauthentication|permitrootlogin|pubkeyauthentication|kbdinteractiveauthentication&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output includes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;permitrootlogin no&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pubkeyauthentication yes&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;passwordauthentication no&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;kbdinteractiveauthentication no&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Bonus Hardening (Optional)&lt;/h2&gt;
&lt;h3&gt;Install fail2ban&lt;/h3&gt;
&lt;p&gt;It reduces brute-force noise and bans abusive IPs automatically.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt&lt;/span&gt;&lt;span&gt; update&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;span&gt; fail2ban&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; enable&lt;/span&gt;&lt;span&gt; --now&lt;/span&gt;&lt;span&gt; fail2ban&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Disable X11 forwarding if you don’t need it&lt;/h3&gt;
&lt;p&gt;In sshd_config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;X11Forwarding no&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Less attack surface is always better.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;For Further Hardening&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;firewall setup&lt;/li&gt;
&lt;li&gt;automatic security upgrades&lt;/li&gt;
&lt;li&gt;monitoring login attempts&lt;/li&gt;
&lt;li&gt;restricting SSH access by IP or using VPN&lt;/li&gt;
&lt;li&gt;adding 2FA&lt;/li&gt;
&lt;li&gt;Secure DNS&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Hardening a Fresh Ubuntu VPS with Secure SSH</title><link>https://michifumi.de/blog/2025-12-22-hardening-a-fresh-ubuntu-vps/</link><guid isPermaLink="true">https://michifumi.de/blog/2025-12-22-hardening-a-fresh-ubuntu-vps/</guid><description>A step-by-step guide to securing a new Ubuntu VPS by creating a regular user, configuring sudo, and disabling root SSH access.</description><pubDate>Mon, 22 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;A step-by-step guide to securing a new Ubuntu VPS by creating a regular user, configuring sudo, and disabling root SSH access.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Nowadays, many cloud service providers offer VPS instances that allow direct SSH access as the root user by default. While convenient for initial setup, this configuration increases the attack surface and deviates from security best practices. This blog walks through hardening a fresh VPS step by step, from creating a regular user account to disabling root SSH login.&lt;/p&gt;
&lt;p&gt;The examples below assume:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu as the operating system&lt;/li&gt;
&lt;li&gt;You initially log in as &lt;code&gt;root&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SSH access is already available&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Why Direct Root SSH Login Is a Problem&lt;/h2&gt;
&lt;p&gt;Allowing direct root login over SSH has several downsides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Larger attack surface&lt;/strong&gt;: attackers know the username (&lt;code&gt;root&lt;/code&gt;) in advance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No accountability&lt;/strong&gt;: all actions appear as root, with no user separation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Higher risk&lt;/strong&gt;: a single compromised password or key grants full system control&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Best practice is to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use a regular user for daily operations&lt;/li&gt;
&lt;li&gt;Escalate privileges only when needed via &lt;code&gt;sudo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Disable root SSH access entirely&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 1: Inspect Existing Users&lt;/h2&gt;
&lt;p&gt;On a fresh VPS, for instance, from RackNerd, you’ll often find that only &lt;code&gt;root&lt;/code&gt; is a real login user.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; /etc/passwd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see only system accounts (with shells like &lt;code&gt;/usr/sbin/nologin&lt;/code&gt;) and &lt;code&gt;root&lt;/code&gt;, you’ll need to create a regular user.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 2: Create a Regular User&lt;/h2&gt;
&lt;p&gt;Create a new user (we’ll call it &lt;code&gt;ubuntu&lt;/code&gt; this time, you can change it to whatever name you like):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;adduser&lt;/span&gt;&lt;span&gt; ubuntu&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creates a home directory (&lt;code&gt;/home/ubuntu&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Assigns a normal UID (≥ 1000)&lt;/li&gt;
&lt;li&gt;Sets &lt;code&gt;/bin/bash&lt;/code&gt; as the login shell&lt;/li&gt;
&lt;li&gt;Prompts you to set a password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you prefer to use a strong, randomly generated password instead of choosing one manually, you can generate it locally with OpenSSL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;openssl&lt;/span&gt;&lt;span&gt; rand&lt;/span&gt;&lt;span&gt; -base64&lt;/span&gt;&lt;span&gt; 32&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Copy the generated password and paste it when prompted by &lt;code&gt;adduser&lt;/code&gt;, or set it afterward using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;passwd&lt;/span&gt;&lt;span&gt; ubuntu&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 3: Grant Sudo Privileges&lt;/h2&gt;
&lt;p&gt;To allow administrative tasks without logging in as root:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;usermod&lt;/span&gt;&lt;span&gt; -aG&lt;/span&gt;&lt;span&gt; sudo&lt;/span&gt;&lt;span&gt; ubuntu&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Test it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;su&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span&gt; ubuntu&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; whoami&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;root&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 4: Verify SSH Access for the New User&lt;/h2&gt;
&lt;p&gt;Before making any SSH changes, &lt;strong&gt;always test&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ssh&lt;/span&gt;&lt;span&gt; ubuntu@localhost&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If this works locally, remote SSH access will work as well.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 5: Disable Root SSH Login&lt;/h2&gt;
&lt;p&gt;Once you’ve confirmed the new user can log in and use sudo, disable root SSH access.&lt;/p&gt;
&lt;p&gt;What you should do is create a drop-in configuration file under &lt;code&gt;/etc/ssh/sshd_config.d/&lt;/code&gt;. Because OpenSSH uses &lt;strong&gt;first match wins&lt;/strong&gt; and evaluates drop-in files alphabetically, naming the file &lt;code&gt;01-hardening.conf&lt;/code&gt; guarantees our setting takes priority over any default cloud-init or provider templates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; tee&lt;/span&gt;&lt;span&gt; /etc/ssh/sshd_config.d/01-hardening.conf&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;EOF&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PermitRootLogin no&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Test syntax before restarting&lt;/h3&gt;
&lt;p&gt;Always test your configuration syntax before restarting the SSH service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; sshd&lt;/span&gt;&lt;span&gt; -t&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No output&lt;/strong&gt;: Silent success — syntax is valid and safe to apply.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Restart SSH service&lt;/h3&gt;
&lt;p&gt;Apply the changes by restarting the SSH service depending on your distribution:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ubuntu 24.04 LTS (Socket-activated SSH):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; ssh.socket&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ubuntu 22.04 LTS / Debian (Classic SSH service):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; ssh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;You can also run &lt;code&gt;sudo systemctl restart ssh 2&amp;gt;/dev/null || sudo systemctl restart ssh.socket&lt;/code&gt; to handle either setup automatically.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Verify root login is disabled&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; sshd&lt;/span&gt;&lt;span&gt; -T&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; grep&lt;/span&gt;&lt;span&gt; permitrootlogin&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;permitrootlogin no&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 6 (Optional but Recommended): Lock the Root Password&lt;/h2&gt;
&lt;p&gt;To prevent &lt;strong&gt;any&lt;/strong&gt; password-based root login:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;passwd&lt;/span&gt;&lt;span&gt; -l&lt;/span&gt;&lt;span&gt; root&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Root access remains available via &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Verify the root account is locked:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;passwd&lt;/span&gt;&lt;span&gt; -S&lt;/span&gt;&lt;span&gt; root&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Expected output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;root L ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Step 7 (Optional): Passwordless sudo for Convenience&lt;/h2&gt;
&lt;p&gt;If you want &lt;code&gt;sudo -i&lt;/code&gt; to switch to root without prompting for a password:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; visudo&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ubuntu ALL=(ALL) NOPASSWD:ALL&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; -i&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;drops you directly into a root shell.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;This trades convenience for security. Use with care.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;Future Improvements&lt;/h2&gt;
&lt;p&gt;The steps covered in this post establish a secure baseline for a freshly provisioned VPS. However, there is still room for further hardening.&lt;/p&gt;
&lt;p&gt;One of the most impactful improvements is switching from password-based SSH authentication to &lt;strong&gt;SSH key–based authentication&lt;/strong&gt;. SSH keys provide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Stronger cryptographic security&lt;/li&gt;
&lt;li&gt;Protection against brute-force password attacks&lt;/li&gt;
&lt;li&gt;Better usability once configured&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using SSH keys will significantly reduce the attack surface of an internet-facing server.&lt;/p&gt;
&lt;p&gt;In a future post, we’ll walk through the process of configuring SSH keys step by step and locking down SSH access even further.&lt;/p&gt;
&lt;p&gt;But until then, I wish you a Merry Christmas 🎄✨🎅&lt;/p&gt;</content:encoded></item><item><title>How I Forced Brave to Use AMD GPU on an Intel Mac</title><link>https://michifumi.de/blog/2025-12-03-force-brave-to-use-discrete-gpu/</link><guid isPermaLink="true">https://michifumi.de/blog/2025-12-03-force-brave-to-use-discrete-gpu/</guid><description>Brave Browser (Chromium) kept sticking to Intel iGPU for WebGL/WebGPU. Here&apos;s the exact fix I used and a one-click Automator launcher to make it permanent.</description><pubDate>Wed, 03 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Brave Browser (Chromium) kept sticking to Intel iGPU for WebGL/WebGPU. Here&apos;s the exact fix I used and a one-click Automator launcher to make it permanent.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;#the-real-fix-force-highperformance-gpu-at-launch&quot;&gt;Go to solution directly&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This issue has been bothering me for over half a year: ever since I got hooked on using the Brave browser, I’ve noticed that on my old 2018 MacBook Pro, it only utilizes the Intel integrated graphics and fails to leverage the discrete AMD GPU for rendering intensive tasks. This forces me to occasionally switch to Safari (WebKit sucks!) when encountering heavy rendering demands to achieve a smoother browsing experience. Today, I stumbled upon news that WebGPU has officially launched in major browsers. On a whim, I decided to tackle this issue.&lt;/p&gt;
&lt;p&gt;First, I confirmed all settings are correct:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Brave &lt;strong&gt;Settings → System → Use graphics acceleration when available&lt;/strong&gt;: &lt;strong&gt;ON&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;macOS &lt;strong&gt;Battery → Automatic graphics switching&lt;/strong&gt;: &lt;strong&gt;OFF&lt;/strong&gt; (forces the system to use the discrete GPU).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Terminal (system‑wide GPU preference):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pmset&lt;/span&gt;&lt;span&gt; -a&lt;/span&gt;&lt;span&gt; gpuswitch&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This forces macOS to use the discrete GPU system‑wide. While this locks the display output to the AMD chip, Chromium’s internal GPU process still defaulted to creating its context on the integrated adapter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reset to default:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; pmset&lt;/span&gt;&lt;span&gt; -a&lt;/span&gt;&lt;span&gt; gpuswitch&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(&lt;code&gt;2&lt;/code&gt; restores automatic switching. The change takes effect immediately, though a reboot can ensure all background processes reload.)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Despite all that, Brave still stubbornly used Intel.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Symptom&lt;/h2&gt;
&lt;p&gt;To reproduce the problem, I used a classic GPU-stressing WebGL demo from &lt;a href=&quot;https://threejs.org/&quot;&gt;Three.js&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;Safari&lt;/strong&gt;, opening the Three.js animation example immediately flipped the machine to the discrete AMD GPU with stable 60 FPS.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Brave&lt;/strong&gt;, the same page stayed on the Intel iGPU and, even under heavy rendering load, barely reached 30 FPS.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I checked the report from &lt;code&gt;brave://gpu&lt;/code&gt;, which showed that Brave was clearly &lt;em&gt;hardware-accelerating&lt;/em&gt;, just on the wrong adapter.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What brave://gpu Revealed&lt;/h2&gt;
&lt;p&gt;I opened &lt;code&gt;brave://gpu&lt;/code&gt; and found something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Both GPUs were detected:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AMD Radeon Pro Vega 20&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intel UHD Graphics 630&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;But Brave marked the Intel chip as &lt;strong&gt;ACTIVE&lt;/strong&gt;, and WebGL reported:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GL_RENDERER: ANGLE Metal Renderer: Intel UHD Graphics 630&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So it wasn’t a software fallback. Chromium was simply choosing the low‑power GPU and sticking to it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Real Fix: Force High‑Performance GPU at Launch&lt;/h2&gt;
&lt;p&gt;Since the Chromium flag to force high‑performance GPU is &lt;strong&gt;not available on Intel dual‑GPU macOS builds&lt;/strong&gt;, there’s no UI toggle that persists this choice.&lt;/p&gt;
&lt;p&gt;Chromium, however, still honors a startup argument that overrides the early GPU selection.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Make sure to quit Brave completely (&lt;code&gt;Cmd + Q&lt;/code&gt;) first.&lt;/strong&gt; On macOS, &lt;code&gt;open -a&lt;/code&gt; will simply bring an already-running instance to the foreground and silently ignore any new &lt;code&gt;--args&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Run this in Terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;open&lt;/span&gt;&lt;span&gt; -a&lt;/span&gt;&lt;span&gt; &quot;Brave Browser&quot;&lt;/span&gt;&lt;span&gt; --args&lt;/span&gt;&lt;span&gt; --force_high_performance_gpu&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After launching Brave this way, &lt;code&gt;brave://gpu&lt;/code&gt; finally showed the AMD GPU as &lt;strong&gt;ACTIVE&lt;/strong&gt;, and the Three.js demo ran just as smoothly as it did in Safari.&lt;/p&gt;
&lt;h3&gt;Why this works&lt;/h3&gt;
&lt;p&gt;Chromium decides which GPU to bind &lt;strong&gt;very early during startup&lt;/strong&gt; for its GPU process. On dual‑GPU Macs it defaults to the integrated Intel chip for battery life, and once that choice is made, most WebGL/WebGPU contexts follow it for the rest of the session. It seems this issue persists to this day (see Chromium issue &lt;a href=&quot;https://issues.chromium.org/issues/393263507&quot;&gt;#393263507&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--force_high_performance_gpu&lt;/code&gt; argument injects a “prefer discrete GPU” directive &lt;em&gt;before&lt;/em&gt; that decision locks in, so the GPU process starts on AMD, and all rendering follows.&lt;/p&gt;
&lt;p&gt;Functionally this is the same behavior the missing &lt;code&gt;force-high-performance-gpu&lt;/code&gt; flag would provide if it were supported on this platform, it’s just applied through a launch argument instead of Brave’s flags UI.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Make It Permanent: One‑Click Automator Launcher&lt;/h2&gt;
&lt;p&gt;Typing the launch command every time is annoying, so I made a tiny Automator app that starts Brave in AMD mode.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Open &lt;strong&gt;Automator&lt;/strong&gt; → &lt;strong&gt;New Document&lt;/strong&gt; → choose &lt;strong&gt;Application&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add &lt;strong&gt;Run Shell Script&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set shell to &lt;code&gt;/bin/zsh&lt;/code&gt; and paste:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;open&lt;/span&gt;&lt;span&gt; -a&lt;/span&gt;&lt;span&gt; &quot;Brave Browser&quot;&lt;/span&gt;&lt;span&gt; --args&lt;/span&gt;&lt;span&gt; --force_high_performance_gpu&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Save as something like &lt;strong&gt;Brave AMD.app&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Drag &lt;strong&gt;Brave AMD.app&lt;/strong&gt; into the Dock and remove the original Brave icon.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;Remember to quit Brave (&lt;code&gt;Cmd + Q&lt;/code&gt;) before clicking the launcher if you are switching modes; macOS won’t apply launch arguments to an existing running process.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Here’s what the Automator setup looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/2025-12-03/automator-setup.png&quot; alt=&quot;Automator launcher setup&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now every time I click the Dock icon, Brave launches using the AMD GPU.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Bonus: Make the Launcher Look Like Brave&lt;/h2&gt;
&lt;p&gt;Because the launcher is a separate app, it has a generic Automator icon by default. I changed it to the Brave icon:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Finder → &lt;strong&gt;Applications&lt;/strong&gt; → right‑click &lt;strong&gt;Brave Browser.app&lt;/strong&gt; → &lt;strong&gt;Get Info&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click the small Brave icon top‑left to highlight it.&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Cmd‑C&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Right‑click &lt;strong&gt;Brave AMD.app&lt;/strong&gt; → &lt;strong&gt;Get Info&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click its small icon top‑left and press &lt;strong&gt;Cmd‑V&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the Dock doesn’t update immediately, remove/re‑add the launcher.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Tradeoffs&lt;/h2&gt;
&lt;p&gt;Forcing the discrete GPU means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;More battery drain&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More heat/fan usage&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For me that’s a fair trade whenever I’m doing heavy WebGL/WebGPU stuff (3D demos, map visualizations, creative coding, etc.). Daily browsing on battery? I can still quit and open the normal Brave app if I want Intel. Not to mention, the battery on my MacBook Pro hasn’t been able to last through a single hour of normal use for ages, I couldn’t care less about battery life.&lt;/p&gt;</content:encoded></item><item><title>Self‑Hosted Analytics for a Personal Blog</title><link>https://michifumi.de/blog/2025-10-31-self-hosted-lightweight-analytics-via-a-remote-endpoint/</link><guid isPermaLink="true">https://michifumi.de/blog/2025-10-31-self-hosted-lightweight-analytics-via-a-remote-endpoint/</guid><description>How I added privacy‑friendly visitor statistics to a static Astro site using a tiny Node.js endpoint, SQLite, PM2, and Caddy.</description><pubDate>Sat, 01 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;How I added privacy‑friendly visitor statistics to a static Astro site using a tiny Node.js endpoint, SQLite, PM2, and Caddy.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;Instead of using third‑party analytics like Cloudflare, I’m running a tiny &lt;strong&gt;self‑hosted&lt;/strong&gt; tracker and you can also learn how it works and replicate it.&lt;/p&gt;
&lt;h2&gt;What I’ve built&lt;/h2&gt;
&lt;p&gt;A tiny analytics API that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accepts page‑view pings from this blog (&lt;code&gt;/track&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Provides comprehensive analytics via &lt;code&gt;/summary&lt;/code&gt; and &lt;code&gt;/daily&lt;/code&gt; endpoints&lt;/li&gt;
&lt;li&gt;Lets me export raw visits as CSV (&lt;code&gt;/export&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Stores data in a single &lt;code&gt;SQLite&lt;/code&gt; file for easy backup/migration&lt;/li&gt;
&lt;li&gt;Runs forever with &lt;code&gt;pm2&lt;/code&gt;, served over HTTPS with &lt;code&gt;Caddy&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can adapt this for any static site (Astro, Hugo, etc.).&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;0) Prerequisites&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A cloud VM with a public IP&lt;/li&gt;
&lt;li&gt;A subdomain for the analytics endpoint&lt;/li&gt;
&lt;li&gt;Basic DNS access (Cloudflare etc.)&lt;/li&gt;
&lt;li&gt;Node.js 18+ and npm&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;p&gt;WARNING&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debian/Ubuntu users&lt;/strong&gt;: Do not use &lt;code&gt;sudo apt install nodejs npm&lt;/code&gt; directly from the default repositories, as they often contain &lt;strong&gt;severely outdated&lt;/strong&gt; Node.js versions (e.g., Node.js 10.x or 12.x) with &lt;strong&gt;known security vulnerabilities&lt;/strong&gt; and &lt;strong&gt;no security patches&lt;/strong&gt;. Always install from NodeSource to get current, supported versions.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Debian/Ubuntu&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt&lt;/span&gt;&lt;span&gt; update&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt&lt;/span&gt;&lt;span&gt; upgrade&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Install latest Node.js LTS from NodeSource&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -fsSL&lt;/span&gt;&lt;span&gt; https://deb.nodesource.com/setup_lts.x&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; sudo&lt;/span&gt;&lt;span&gt; -E&lt;/span&gt;&lt;span&gt; bash&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;span&gt; nodejs&lt;/span&gt;&lt;span&gt; git&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Verify installation&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;node&lt;/span&gt;&lt;span&gt; --version&lt;/span&gt;&lt;span&gt;  # Should show the latest LTS version&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; --version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fedora CoreOS&lt;/h3&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;Fedora CoreOS is an &lt;strong&gt;immutable operating system&lt;/strong&gt; designed for containerized workloads. You cannot install packages directly with &lt;code&gt;dnf install&lt;/code&gt;. Instead, use &lt;strong&gt;toolbox&lt;/strong&gt; to create a mutable container environment.&lt;/p&gt;
&lt;p&gt;For more information about Fedora CoreOS on GCP, see the &lt;a href=&quot;https://docs.fedoraproject.org/en-US/fedora-coreos/provisioning-gcp/&quot;&gt;official documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Step 1: Create and enter a toolbox&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;toolbox&lt;/span&gt;&lt;span&gt; create&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;toolbox&lt;/span&gt;&lt;span&gt; enter&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Install Node.js inside the toolbox&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Install Node.js from Fedora&apos;s default repositories&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; dnf&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;span&gt; nodejs&lt;/span&gt;&lt;span&gt; npm&lt;/span&gt;&lt;span&gt; git&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Verify installation&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;node&lt;/span&gt;&lt;span&gt; --version&lt;/span&gt;&lt;span&gt;  # Should show v22.x.x&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; --version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Work inside the toolbox&lt;/h3&gt;
&lt;p&gt;All subsequent commands (creating the project, installing packages, running the server) should be executed &lt;strong&gt;inside the toolbox&lt;/strong&gt;. The toolbox persists across reboots and you can re-enter it anytime with &lt;code&gt;toolbox enter&lt;/code&gt;.&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;To exit the toolbox, type &lt;code&gt;exit&lt;/code&gt;. To re-enter later, use &lt;code&gt;toolbox enter&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;1) Create the analytics service&lt;/h2&gt;
&lt;p&gt;Create a new folder and initialise a Node project:&lt;/p&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;I use &lt;code&gt;better-sqlite3&lt;/code&gt; instead of &lt;code&gt;sqlite3&lt;/code&gt; to avoid npm vulnerabilities and get better performance. It’s synchronous (simpler) and has fewer security issues.&lt;/p&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/page-stats/data&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp; &lt;/span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; ~/page-stats&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; init&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; express&lt;/span&gt;&lt;span&gt; better-sqlite3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create &lt;code&gt;server.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; fs&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;fs&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; express&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;express&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; Database&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;better-sqlite3&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; app&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; express&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;trust proxy&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;json spaces&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;fs.&lt;/span&gt;&lt;span&gt;mkdirSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;./data&quot;&lt;/span&gt;&lt;span&gt;, { recursive: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; db&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Database&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;./data/stats.db&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(express.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ limit: &lt;/span&gt;&lt;span&gt;&quot;2kb&quot;&lt;/span&gt;&lt;span&gt; }));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;use&lt;/span&gt;&lt;span&gt;(express.&lt;/span&gt;&lt;span&gt;text&lt;/span&gt;&lt;span&gt;({ type: &lt;/span&gt;&lt;span&gt;&quot;text/plain&quot;&lt;/span&gt;&lt;span&gt;, limit: &lt;/span&gt;&lt;span&gt;&quot;2kb&quot;&lt;/span&gt;&lt;span&gt; }));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;db.&lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  PRAGMA journal_mode = WAL;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  PRAGMA synchronous = NORMAL;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  PRAGMA busy_timeout = 5000;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;db.&lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`CREATE TABLE IF NOT EXISTS visits (&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  id INTEGER PRIMARY KEY AUTOINCREMENT,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  path TEXT,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  referrer TEXT,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ua TEXT,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ip TEXT,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ts DATETIME DEFAULT CURRENT_TIMESTAMP&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;)`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;db.&lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`CREATE INDEX IF NOT EXISTS idx_visits_path ON visits(path)`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;db.&lt;/span&gt;&lt;span&gt;exec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`CREATE INDEX IF NOT EXISTS idx_visits_ts ON visits(ts)`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; stmtInsert&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;prepare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  `INSERT INTO visits (path, referrer, ua, ip) VALUES (?, ?, ?, ?)`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; stmtExport&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;prepare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  `SELECT * FROM visits ORDER BY ts DESC`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; stmtDaily&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;prepare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  SELECT path, ip, COUNT(*) AS views, MAX(ts) AS last_seen&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  FROM visits&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  WHERE ts &amp;gt;= DATE(&apos;now&apos;, &apos;-30 days&apos;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  GROUP BY DATE(ts), path, ip&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ORDER BY last_seen DESC&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; stmtSummaryTotal&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;prepare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  SELECT&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    COUNT(*) AS total_views,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    COUNT(DISTINCT path) AS unique_paths,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    COUNT(DISTINCT ip) AS unique_visitors,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MIN(ts) AS first_visit,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MAX(ts) AS last_visit&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  FROM visits&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; stmtSummaryByPath&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;prepare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  SELECT path, COUNT(*) AS views&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  FROM visits&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  GROUP BY path&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ORDER BY views DESC&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; stmtSummaryByDay&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;prepare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  SELECT DATE(ts) AS day, COUNT(*) AS views&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  FROM visits&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  GROUP BY day&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ORDER BY day DESC&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; stmtSummaryByIp&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; db.&lt;/span&gt;&lt;span&gt;prepare&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  SELECT ip, COUNT(*) AS views&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  FROM visits&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  GROUP BY ip&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ORDER BY views DESC&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/track&quot;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  let&lt;/span&gt;&lt;span&gt; body &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; req.body;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  // Handle both text/plain and JSON input&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; body &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &quot;string&quot;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      body &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;parse&lt;/span&gt;&lt;span&gt;(body);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      body &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;path&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;rawPath&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;referrer&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;rawReferrer&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; body &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; {};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; path&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; typeof&lt;/span&gt;&lt;span&gt; rawPath &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &quot;string&quot;&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span&gt; rawPath &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; referrer&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; typeof&lt;/span&gt;&lt;span&gt; rawReferrer &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &quot;string&quot;&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span&gt; rawReferrer &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  // Always read user-agent from the real HTTP header — never trust the client body&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; userAgent&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; req.headers[&lt;/span&gt;&lt;span&gt;&quot;user-agent&quot;&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; ip&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; req.ip &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;path) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    console.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;[analytics] Missing path field in request body&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; res.&lt;/span&gt;&lt;span&gt;sendStatus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;400&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    stmtInsert.&lt;/span&gt;&lt;span&gt;run&lt;/span&gt;&lt;span&gt;(path, referrer, userAgent, ip);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;sendStatus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;204&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    console.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;DB insert error:&quot;&lt;/span&gt;&lt;span&gt;, err);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;sendStatus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// Escape a value for safe CSV output&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// Doubles any internal quotes (&quot;  →  &quot;&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// Prefixes values starting with =, +, -, @, tab, or CR with a tab to&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// neutralise spreadsheet formula injection (CVE-class: CSV injection)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; csvEscape&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;val&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; str&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; String&lt;/span&gt;&lt;span&gt;(val &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; safe&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; /&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;[=+\-@\t\r]&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;(str) &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; `&lt;/span&gt;&lt;span&gt;\t&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;str&lt;/span&gt;&lt;span&gt;}`&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; str;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; `&quot;${&lt;/span&gt;&lt;span&gt;safe&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;&quot;&quot;&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&quot;`&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// CSV export&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/export&quot;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; rows&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; stmtExport.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; csv&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;id,path,referrer,ua,ip,ts&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ...&lt;/span&gt;&lt;span&gt;rows.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        [r.id, r.path, r.referrer, r.ua, r.ip, r.ts]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          .&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;((&lt;/span&gt;&lt;span&gt;v&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;i&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; i &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span&gt; r.id &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; csvEscape&lt;/span&gt;&lt;span&gt;(v))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          .&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;,&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      )&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ].&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;\n&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;setHeader&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Content-Disposition&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;attachment; filename=stats.csv&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;type&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;text/csv&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;(csv);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;send&lt;/span&gt;&lt;span&gt;(err.message);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// --- Daily summary (views per day and path, last 30 days) ---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/daily&quot;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;(stmtDaily.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ error: err.message });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// --- Totals summary (overall + by path + by day, all-time) ---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/summary&quot;&lt;/span&gt;&lt;span&gt;, (&lt;/span&gt;&lt;span&gt;req&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;res&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ...&lt;/span&gt;&lt;span&gt;stmtSummaryTotal.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      by_path: stmtSummaryByPath.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      by_day:  stmtSummaryByDay.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      by_ip:   stmtSummaryByIp.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    res.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;({ error: err.message });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;app.&lt;/span&gt;&lt;span&gt;listen&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;8080&lt;/span&gt;&lt;span&gt;, () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; console.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Analytics server running on port 8080&quot;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quick test:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;node&lt;/span&gt;&lt;span&gt; server.js&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In another shell:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -X&lt;/span&gt;&lt;span&gt; POST&lt;/span&gt;&lt;span&gt; http://localhost:8080/track&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; &quot;Content-Type: text/plain&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -d&lt;/span&gt;&lt;span&gt; &apos;{&quot;path&quot;:&quot;/hello&quot;,&quot;referrer&quot;:&quot;&quot;}&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &quot;http://localhost:8080/summary&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see a JSON object with total views and breakdowns.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2) Keep it running in the background&lt;/h2&gt;
&lt;h3&gt;Debian/Ubuntu: pm2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; npm&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; -g&lt;/span&gt;&lt;span&gt; pm2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; start&lt;/span&gt;&lt;span&gt; server.js&lt;/span&gt;&lt;span&gt; --name&lt;/span&gt;&lt;span&gt; stats&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; startup&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Run the one-line command pm2 prints for systemd, then:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; save&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check status:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; ls&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stop the service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; stop&lt;/span&gt;&lt;span&gt; stats&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restart the service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; stats&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Remove from PM2 (stops and removes from process list):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; delete&lt;/span&gt;&lt;span&gt; stats&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Completely disable PM2 auto-start (removes systemd integration):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; unstartup&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Run the command it suggests with sudo&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or manually disable the systemd service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; disable&lt;/span&gt;&lt;span&gt; pm2-&lt;/span&gt;&lt;span&gt;$USER&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; stop&lt;/span&gt;&lt;span&gt; pm2-&lt;/span&gt;&lt;span&gt;$USER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fedora CoreOS: Podman + Systemd&lt;/h3&gt;
&lt;h4&gt;Step 1: Create a Dockerfile (inside toolbox)&lt;/h4&gt;
&lt;p&gt;Inside toolbox, in ~/page-stats directory&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; Dockerfile&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;EOF&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;FROM node:22-alpine&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;WORKDIR /app&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;COPY package*.json ./&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;RUN npm install --production&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;COPY server.js ./&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;RUN mkdir -p /app/data&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EXPOSE 8080&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;CMD [&quot;node&quot;, &quot;server.js&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 2: Exit toolbox and build the container image on the host&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;exit&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On the Fedora CoreOS host, build the image&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; ~/page-stats&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;podman&lt;/span&gt;&lt;span&gt; build&lt;/span&gt;&lt;span&gt; -t&lt;/span&gt;&lt;span&gt; localhost/page-stats:latest&lt;/span&gt;&lt;span&gt; .&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 3: Create a data directory for persistence&lt;/h4&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why use a separate data directory:&lt;/strong&gt; Storing the database in &lt;code&gt;~/page-stats/data/&lt;/code&gt; keeps it outside the container image, so it persists across container restarts. The application code (&lt;code&gt;server.js&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt;) stays in the image, so rebuilds with &lt;code&gt;podman build&lt;/code&gt; always take effect.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Create the data directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/page-stats/data&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 4: Create systemd service&lt;/h4&gt;
&lt;p&gt;Create systemd user service directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/.config/systemd/user/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the service file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; ~/.config/systemd/user/page-stats.service&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;EOF&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Unit]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Description=Page Stats Analytics Service&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;After=network-online.target&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Wants=network-online.target&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Service]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Type=simple&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Restart=always&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;RestartSec=10&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ExecStartPre=-/usr/bin/podman rm -f -i page-stats&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ExecStart=/usr/bin/podman run --rm --name page-stats \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -p 8080:8080 \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -v %h/page-stats/data:/app/data:Z \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  localhost/page-stats:latest&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ExecStop=/usr/bin/podman stop -t 10 page-stats&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Install]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;WantedBy=default.target&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 5: Enable and start the service&lt;/h4&gt;
&lt;p&gt;Reload systemd:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; daemon-reload&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enable service to start on boot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;loginctl&lt;/span&gt;&lt;span&gt; enable-linger&lt;/span&gt;&lt;span&gt; $USER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; enable&lt;/span&gt;&lt;span&gt; --now&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check status:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stop the service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; stop&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restart the service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Disable and stop (removes from startup):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; disable&lt;/span&gt;&lt;span&gt; --now&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 6: View logs&lt;/h4&gt;
&lt;p&gt;Follow logs in real-time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;journalctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;View recent logs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;journalctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;span&gt; -n&lt;/span&gt;&lt;span&gt; 50&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Updating the code&lt;/strong&gt;: If you modify &lt;code&gt;server.js&lt;/code&gt; (e.g., changing the password), you must rebuild the container image and restart the service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; ~/page-stats&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;podman&lt;/span&gt;&lt;span&gt; build&lt;/span&gt;&lt;span&gt; -t&lt;/span&gt;&lt;span&gt; localhost/page-stats:latest&lt;/span&gt;&lt;span&gt; .&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; restart&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The container runs a snapshot of your code from when it was built, not the live file.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;3) Obtain HTTPS with Caddy (reverse proxy)&lt;/h2&gt;
&lt;h3&gt;Debian/Ubuntu: Install Caddy&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://caddyserver.com/docs/install&quot;&gt;Caddy install guide&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Fedora CoreOS: Install Caddy Static Binary&lt;/h3&gt;
&lt;p&gt;Download and install Caddy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -o&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; &apos;https://caddyserver.com/api/download?os=linux&amp;amp;arch=amd64&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;chmod&lt;/span&gt;&lt;span&gt; +x&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Move to system location:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; mv&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; /usr/local/bin/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify installation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;caddy&lt;/span&gt;&lt;span&gt; version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create Caddy user and group:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; groupadd&lt;/span&gt;&lt;span&gt; --system&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; useradd&lt;/span&gt;&lt;span&gt; --system&lt;/span&gt;&lt;span&gt; --gid&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; --create-home&lt;/span&gt;&lt;span&gt; --home-dir&lt;/span&gt;&lt;span&gt; /var/lib/caddy&lt;/span&gt;&lt;span&gt; --shell&lt;/span&gt;&lt;span&gt; /usr/sbin/nologin&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create systemd service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; tee&lt;/span&gt;&lt;span&gt; /etc/systemd/system/caddy.service&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; /dev/null&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;EOF&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Unit]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Description=Caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Documentation=https://caddyserver.com/docs/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;After=network.target network-online.target&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Requires=network-online.target&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Service]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Type=notify&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;User=caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Group=caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile --force&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;TimeoutStopSec=5s&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;LimitNOFILE=1048576&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PrivateTmp=true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ProtectSystem=full&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Install]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;WantedBy=multi-user.target&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Create log directory&lt;/h3&gt;
&lt;p&gt;Before configuring Caddy, create the log directory with correct permissions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; /var/log/caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; chown&lt;/span&gt;&lt;span&gt; -R&lt;/span&gt;&lt;span&gt; caddy:caddy&lt;/span&gt;&lt;span&gt; /var/log/caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; chmod&lt;/span&gt;&lt;span&gt; 755&lt;/span&gt;&lt;span&gt; /var/log/caddy&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Generate a bcrypt password hash&lt;/h3&gt;
&lt;p&gt;Before writing the Caddyfile, generate the hashed password Caddy requires for &lt;code&gt;basic_auth&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;caddy&lt;/span&gt;&lt;span&gt; hash-password&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Enter your password at the prompt.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Copy the $2a$14$... string it outputs — you&apos;ll paste it into the Caddyfile.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;Never paste a plain-text password into the Caddyfile. Caddy only accepts bcrypt hashes here.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Configure Caddyfile&lt;/h3&gt;
&lt;p&gt;Then configure &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; nano&lt;/span&gt;&lt;span&gt; /etc/caddy/Caddyfile&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Replace with your own domain and congratulations you have found my analytics domain ;-)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Feel free to block it by using uBlock Origin if you don&apos;t want me to know you are stalking me&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;stats.zaku.eu.org {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        log {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                output file /var/log/caddy/stats-access.log {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        roll_size 10MB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        roll_keep 10&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        roll_keep_for 720h&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        header {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Access-Control-Allow-Origin &quot;https://michifumi.de&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Access-Control-Allow-Methods &quot;GET, POST, OPTIONS, HEAD&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Access-Control-Allow-Headers &quot;Content-Type, Authorization&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Access-Control-Max-Age &quot;86400&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Strict-Transport-Security &quot;max-age=31536000; includeSubDomains; preload&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                X-Content-Type-Options &quot;nosniff&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                X-Frame-Options &quot;DENY&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Referrer-Policy &quot;strict-origin-when-cross-origin&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        @options method OPTIONS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        handle @options {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                respond 204&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        # /track is public — the blog sends beacons here without credentials&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        handle /track {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                reverse_proxy localhost:8080&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        # All other endpoints (/summary, /daily, /export) require login&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        @protected not path /track&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        handle @protected {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                basic_auth {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        # Replace with your own username and the hash from caddy hash-password&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        admin $2a$14$REPLACE_WITH_YOUR_HASH&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                reverse_proxy localhost:8080&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;If ports 80/443 are already in use, you can run Caddy on alternate ports, and for a publicly trusted TLS cert on non-443, you typically need DNS-01 validation (see below optional).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Safely updating Caddy configurations&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; fmt&lt;/span&gt;&lt;span&gt; --overwrite&lt;/span&gt;&lt;span&gt; /etc/caddy/Caddyfile&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; validate&lt;/span&gt;&lt;span&gt; --config&lt;/span&gt;&lt;span&gt; /etc/caddy/Caddyfile&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start the service and check status:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; daemon-reload&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; enable&lt;/span&gt;&lt;span&gt; --now&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; -l&lt;/span&gt;&lt;span&gt; --no-pager&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use reload after the service is running and you make future changes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; reload&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Optional: DNS-01 with Cloudflare (when 80/443 are busy)&lt;/h3&gt;
&lt;p&gt;If you cannot free ports 80/443, use DNS-01 so Let’s Encrypt validates via DNS. This requires a Caddy build with the Cloudflare DNS module.&lt;/p&gt;
&lt;h4&gt;Install Go (latest stable version)&lt;/h4&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;Visit &lt;a href=&quot;https://go.dev/dl/&quot;&gt;https://go.dev/dl/&lt;/a&gt; to find the latest stable version. Replace &lt;code&gt;1.26.6&lt;/code&gt; below with the current version number.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Debian/Ubuntu:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; apt&lt;/span&gt;&lt;span&gt; remove&lt;/span&gt;&lt;span&gt; -y&lt;/span&gt;&lt;span&gt; golang-go&lt;/span&gt;&lt;span&gt; golang&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; /tmp&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Replace 1.26.6 with the latest version from https://go.dev/dl/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -LO&lt;/span&gt;&lt;span&gt; https://go.dev/dl/go1.26.6.linux-amd64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; rm&lt;/span&gt;&lt;span&gt; -rf&lt;/span&gt;&lt;span&gt; /usr/local/go&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; tar&lt;/span&gt;&lt;span&gt; -C&lt;/span&gt;&lt;span&gt; /usr/local&lt;/span&gt;&lt;span&gt; -xzf&lt;/span&gt;&lt;span&gt; go1.26.6.linux-amd64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &apos;export PATH=/usr/local/go/bin:$PATH&apos;&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; sudo&lt;/span&gt;&lt;span&gt; tee&lt;/span&gt;&lt;span&gt; /etc/profile.d/go.sh&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt;/dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;source&lt;/span&gt;&lt;span&gt; /etc/profile.d/go.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fedora CoreOS:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; /tmp&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Replace 1.26.6 with the latest version from https://go.dev/dl/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -LO&lt;/span&gt;&lt;span&gt; https://go.dev/dl/go1.26.6.linux-amd64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; rm&lt;/span&gt;&lt;span&gt; -rf&lt;/span&gt;&lt;span&gt; /usr/local/go&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; tar&lt;/span&gt;&lt;span&gt; -C&lt;/span&gt;&lt;span&gt; /usr/local&lt;/span&gt;&lt;span&gt; -xzf&lt;/span&gt;&lt;span&gt; go1.26.6.linux-amd64.tar.gz&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &apos;export PATH=/usr/local/go/bin:$PATH&apos;&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; sudo&lt;/span&gt;&lt;span&gt; tee&lt;/span&gt;&lt;span&gt; /etc/profile.d/go.sh&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; /dev/null&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;source&lt;/span&gt;&lt;span&gt; /etc/profile.d/go.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; version&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Lock Go to the local toolchain&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; env&lt;/span&gt;&lt;span&gt; -w&lt;/span&gt;&lt;span&gt; GOTOOLCHAIN=local&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; env&lt;/span&gt;&lt;span&gt; -w&lt;/span&gt;&lt;span&gt; GOPROXY=https://proxy.golang.org,direct&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Install xcaddy&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; github.com/caddyserver/xcaddy/cmd/xcaddy@latest&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; PATH&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$PATH&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/go/bin&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Build Caddy with the Cloudflare DNS module&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;xcaddy&lt;/span&gt;&lt;span&gt; build&lt;/span&gt;&lt;span&gt; --with&lt;/span&gt;&lt;span&gt; github.com/caddy-dns/cloudflare&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Replace the Caddy binary&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; stop&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; -m&lt;/span&gt;&lt;span&gt; 0755&lt;/span&gt;&lt;span&gt; ./caddy&lt;/span&gt;&lt;span&gt; /usr/local/bin/caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; start&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Verify the module exists&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;caddy&lt;/span&gt;&lt;span&gt; list-modules&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; grep&lt;/span&gt;&lt;span&gt; cloudflare&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Create a Cloudflare API token with&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Zone.Zone:Read&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Zone.DNS:Edit&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Scope it to your zone.&lt;/p&gt;
&lt;h4&gt;Add the token to the Caddy systemd service&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; edit&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;[Service]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Environment&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;CLOUDFLARE_API_TOKEN&lt;/span&gt;&lt;span&gt;=YOUR_TOKEN_HERE&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; daemon-reload&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Update the Caddyfile&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        http_port 8081&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        https_port 8443&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;stats.zaku.eu.org {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        tls {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                dns cloudflare {env.CLOUDFLARE_API_TOKEN}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        log {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                output file /var/log/caddy/stats-access.log {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        roll_size 10MB&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        roll_keep 10&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        roll_keep_for 720h&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        header {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Access-Control-Allow-Origin &quot;https://michifumi.de&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Access-Control-Allow-Methods &quot;GET, POST, OPTIONS, HEAD&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Access-Control-Allow-Headers &quot;Content-Type, Authorization&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Access-Control-Max-Age &quot;86400&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Strict-Transport-Security &quot;max-age=31536000; includeSubDomains; preload&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                X-Content-Type-Options &quot;nosniff&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                X-Frame-Options &quot;DENY&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                Referrer-Policy &quot;strict-origin-when-cross-origin&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        @options method OPTIONS&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        handle @options {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                respond 204&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        # /track is public — the blog sends beacons here without credentials&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        handle /track {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                reverse_proxy localhost:8080&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        # All other endpoints (/summary, /daily, /export) require login&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        @protected not path /track&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        handle @protected {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                basic_auth {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                        admin $2a$14$REPLACE_WITH_YOUR_HASH&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;                reverse_proxy localhost:8080&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With &lt;code&gt;https_port 8443&lt;/code&gt; set, access the API at &lt;code&gt;https://stats.zaku.eu.org:8443&lt;/code&gt; and update your tracking endpoint to include &lt;code&gt;:8443&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Validate and reload&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; fmt&lt;/span&gt;&lt;span&gt; --overwrite&lt;/span&gt;&lt;span&gt; /etc/caddy/Caddyfile&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; validate&lt;/span&gt;&lt;span&gt; --config&lt;/span&gt;&lt;span&gt; /etc/caddy/Caddyfile&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; reload&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; systemctl&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; -l&lt;/span&gt;&lt;span&gt; --no-pager&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Confirm issuance&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; journalctl&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; caddy&lt;/span&gt;&lt;span&gt; -f&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4) DNS (Cloudflare)&lt;/h2&gt;
&lt;p&gt;Add an &lt;strong&gt;A&lt;/strong&gt; record:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;stats&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Target: your VM public IP&lt;/li&gt;
&lt;li&gt;Proxy status: &lt;strong&gt;DNS only&lt;/strong&gt; (gray cloud)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Caddy will fetch a Let’s Encrypt certificate automatically.&lt;br /&gt;
After issuance, HTTPS works at &lt;code&gt;https://stats.zaku.eu.org&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5) Add the tracking snippet to the blog (Astro)&lt;/h2&gt;
&lt;p&gt;Place this near the bottom of your frontend code, such as &lt;code&gt;BaseLayout.astro&lt;/code&gt; (before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt; is:inline&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  (() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;typeof&lt;/span&gt;&lt;span&gt; window &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &apos;undefined&apos;&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; typeof&lt;/span&gt;&lt;span&gt; navigator &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &apos;undefined&apos;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; endpoint&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &apos;https://stats.zaku.eu.org/track&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; payload&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      path: window.location.pathname,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      referrer: document.referrer &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      if&lt;/span&gt;&lt;span&gt; (navigator.sendBeacon) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const&lt;/span&gt;&lt;span&gt; blob&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Blob&lt;/span&gt;&lt;span&gt;([payload], { type: &lt;/span&gt;&lt;span&gt;&apos;text/plain&apos;&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const&lt;/span&gt;&lt;span&gt; ok&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; navigator.&lt;/span&gt;&lt;span&gt;sendBeacon&lt;/span&gt;&lt;span&gt;(endpoint, blob);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        if&lt;/span&gt;&lt;span&gt; (ok) &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      fetch&lt;/span&gt;&lt;span&gt;(endpoint, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        method: &lt;/span&gt;&lt;span&gt;&apos;POST&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        body: payload,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        keepalive: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        mode: &lt;/span&gt;&lt;span&gt;&apos;no-cors&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        headers: { &lt;/span&gt;&lt;span&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;text/plain&apos;&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }).&lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;err&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; console.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;[analytics] fetch failed&apos;&lt;/span&gt;&lt;span&gt;, err));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (err) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      console.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;[analytics] unexpected error&apos;&lt;/span&gt;&lt;span&gt;, err);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  })();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;script&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;NOTE&lt;/p&gt;
&lt;p&gt;Privacy-focused browsers like &lt;strong&gt;Mullvad Browser&lt;/strong&gt; and &lt;strong&gt;Tor Browser&lt;/strong&gt; will block this tracking script by default. Users with ad blockers or privacy extensions will also not be tracked.&lt;/p&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;6) Verify end-to-end&lt;/h2&gt;
&lt;p&gt;From the browser:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Visit the blog.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The analytics API includes three useful endpoints for viewing detailed statistics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/daily&lt;/code&gt;&lt;/strong&gt;: Returns daily visit counts per path for the &lt;strong&gt;last 30 days&lt;/strong&gt;. Useful for tracking recent trends over time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/summary&lt;/code&gt;&lt;/strong&gt;: Returns &lt;strong&gt;all-time totals&lt;/strong&gt; plus breakdowns by path and by day, suitable for comprehensive dashboard overviews.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;/export&lt;/code&gt;&lt;/strong&gt;: Downloads all raw visit data as CSV for backup or analysis in external tools.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These endpoints make it easy to visualise daily activity or build a simple dashboard.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;/daily&lt;/code&gt; endpoint&lt;/h3&gt;
&lt;p&gt;Returns a JSON array with daily stats for each path, sorted by the most recent activity first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;path&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;ip&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;203.0.113.10&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;15&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;last_seen&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2025-10-31 23:42:07&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;path&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/blog1&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;ip&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;198.51.100.5&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;last_seen&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2025-10-30 21:15:33&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;path&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;ip&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;203.0.113.10&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;8&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;last_seen&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2025-10-30 18:30:12&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;path&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/blog2&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;ip&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;203.0.113.10&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;last_seen&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2025-10-30 14:05:44&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Query it with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; admin&lt;/span&gt;&lt;span&gt; &quot;https://stats.zaku.eu.org/daily&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;/summary&lt;/code&gt; endpoint&lt;/h3&gt;
&lt;p&gt;Returns top-level summary stats plus breakdowns:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;total_views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;1234&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;unique_paths&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;21&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;unique_visitors&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;34&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;first_visit&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2025-10-01 09:00:00&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;last_visit&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2025-10-31 11:45:12&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;by_path&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;path&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;800&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;path&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;/blog1&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;120&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;by_day&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;day&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2025-10-31&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;45&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;day&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;2025-10-30&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;38&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;by_ip&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;ip&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;203.0.113.10&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;650&lt;/span&gt;&lt;span&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    { &lt;/span&gt;&lt;span&gt;&quot;ip&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;198.51.100.5&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;views&quot;&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;340&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Query it with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; admin&lt;/span&gt;&lt;span&gt; &quot;https://stats.zaku.eu.org/summary&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSV export:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; admin&lt;/span&gt;&lt;span&gt; -L&lt;/span&gt;&lt;span&gt; -o&lt;/span&gt;&lt;span&gt; stats.csv&lt;/span&gt;&lt;span&gt; &quot;https://stats.zaku.eu.org/export&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;p&gt;TIP&lt;/p&gt;
&lt;p&gt;All analytics endpoints can be accessed directly from your browser. Caddy will show a native login prompt — enter your &lt;code&gt;admin&lt;/code&gt; username and password.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://stats.zaku.eu.org/daily&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://stats.zaku.eu.org/summary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://stats.zaku.eu.org/export&lt;/code&gt; (downloads CSV)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;h2&gt;7) Data Migration&lt;/h2&gt;
&lt;p&gt;All analytics live in &lt;code&gt;stats.db&lt;/code&gt;. To migrate to a new VM:&lt;/p&gt;
&lt;h3&gt;Debian/Ubuntu (PM2)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;On the old VPS:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; stop&lt;/span&gt;&lt;span&gt; stats&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Checkpoint WAL to merge all data into the main database file&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;sqlite3&lt;/span&gt;&lt;span&gt; ~/page-stats/data/stats.db&lt;/span&gt;&lt;span&gt; &quot;PRAGMA wal_checkpoint(TRUNCATE);&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;On your local machine:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;scp&lt;/span&gt;&lt;span&gt; user@OLD_VPS_IP:~/page-stats/data/stats.db&lt;/span&gt;&lt;span&gt; ~/Downloads/stats.db&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;scp&lt;/span&gt;&lt;span&gt; ~/Downloads/stats.db&lt;/span&gt;&lt;span&gt; user@NEW_VPS_IP:~/page-stats/data/stats.db&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;On the new VPS:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pm2&lt;/span&gt;&lt;span&gt; start&lt;/span&gt;&lt;span&gt; ~/page-stats/server.js&lt;/span&gt;&lt;span&gt; --name&lt;/span&gt;&lt;span&gt; stats&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fedora CoreOS (Podman)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;On the old VPS:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Stop the service&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; stop&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Back up the database&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/backups&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; ~/page-stats/data/stats.db&lt;/span&gt;&lt;span&gt; ~/backups/stats.db&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;On your local machine:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;scp&lt;/span&gt;&lt;span&gt; user@OLD_VPS_IP:~/backups/stats.db&lt;/span&gt;&lt;span&gt; ~/Downloads/stats.db&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;scp&lt;/span&gt;&lt;span&gt; ~/Downloads/stats.db&lt;/span&gt;&lt;span&gt; user@NEW_VPS_IP:~/backups/stats.db&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;On the new VPS:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Create the data directory and import the database&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/page-stats/data&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; ~/backups/stats.db&lt;/span&gt;&lt;span&gt; ~/page-stats/data/stats.db&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Start the service&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; start&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;8) Backup and Data Safety&lt;/h2&gt;
&lt;div&gt;
&lt;p&gt;WARNING&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Always maintain regular backups!&lt;/strong&gt; System updates, hardware failures, or accidental deletions can cause data loss.&lt;/p&gt;
&lt;/div&gt;
&lt;h3&gt;Automated CSV Export Backup&lt;/h3&gt;
&lt;p&gt;Set up a daily backup using the &lt;code&gt;/export&lt;/code&gt; endpoint. This works on &lt;strong&gt;both Debian/Ubuntu and Fedora CoreOS&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create a backup script:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; ~/backups&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;nano&lt;/span&gt;&lt;span&gt; ~/backups/backup-stats.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following content (replace &lt;code&gt;YOUR_PASSWORD&lt;/code&gt; with your actual password — it will be read from the environment variable &lt;code&gt;STATS_PASSWORD&lt;/code&gt; so it never appears in the log file):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;#!/bin/bash&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;BACKUP_DIR&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;$HOME&lt;/span&gt;&lt;span&gt;/backups/analytics&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; -p&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$BACKUP_DIR&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Export CSV from the analytics endpoint using basic_auth&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Set STATS_PASSWORD in your environment or systemd unit&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -fsSL&lt;/span&gt;&lt;span&gt; -u&lt;/span&gt;&lt;span&gt; &quot;admin:${&lt;/span&gt;&lt;span&gt;STATS_PASSWORD&lt;/span&gt;&lt;span&gt;}&quot;&lt;/span&gt;&lt;span&gt; &quot;https://stats.zaku.eu.org/export&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -o&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$BACKUP_DIR&lt;/span&gt;&lt;span&gt;/stats-$(&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt; +%Y-%m-%d).csv&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Keep only last 30 days of backups&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;find&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;$BACKUP_DIR&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; -name&lt;/span&gt;&lt;span&gt; &quot;stats-*.csv&quot;&lt;/span&gt;&lt;span&gt; -mtime&lt;/span&gt;&lt;span&gt; +30&lt;/span&gt;&lt;span&gt; -delete&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;echo&lt;/span&gt;&lt;span&gt; &quot;Backup completed: $(&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;)&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;chmod&lt;/span&gt;&lt;span&gt; +x&lt;/span&gt;&lt;span&gt; ~/backups/backup-stats.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Debian/Ubuntu: Schedule with cron (daily at 2 AM):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;crontab&lt;/span&gt;&lt;span&gt; -e&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;0 2 * * * STATS_PASSWORD=&quot;YOUR_PASSWORD_HERE&quot; /home/YOUR_USERNAME/backups/backup-stats.sh &amp;gt;&amp;gt; /home/YOUR_USERNAME/backups/backup.log 2&amp;gt;&amp;amp;1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Fedora CoreOS: Schedule with systemd timer (daily at 2 AM):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create the service unit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; ~/.config/systemd/user/backup-stats.service&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;EOF&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Unit]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Description=Backup analytics CSV&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Service]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Type=oneshot&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Environment=STATS_PASSWORD=YOUR_PASSWORD_HERE&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ExecStart=%h/backups/backup-stats.sh&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the timer unit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cat&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt; ~/.config/systemd/user/backup-stats.timer&lt;/span&gt;&lt;span&gt; &amp;lt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;EOF&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Unit]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Description=Daily analytics backup&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Timer]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;OnCalendar=*-*-* 02:00:00&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Persistent=true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;[Install]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;WantedBy=timers.target&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;EOF&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enable and start the timer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; daemon-reload&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; enable&lt;/span&gt;&lt;span&gt; --now&lt;/span&gt;&lt;span&gt; backup-stats.timer&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify the timer is active:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; list-timers&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fedora CoreOS: Backup the Database File&lt;/h3&gt;
&lt;p&gt;On Fedora CoreOS, the database is stored directly at &lt;code&gt;~/page-stats/data/stats.db&lt;/code&gt;. You can back it up with a simple copy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Stop the service&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; stop&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Copy the database&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;cp&lt;/span&gt;&lt;span&gt; ~/page-stats/data/stats.db&lt;/span&gt;&lt;span&gt; ~/backups/stats-&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt; +%Y-%m-%d&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.db&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Start the service&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;systemctl&lt;/span&gt;&lt;span&gt; --user&lt;/span&gt;&lt;span&gt; start&lt;/span&gt;&lt;span&gt; page-stats.service&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Debian/Ubuntu: Direct Database Backup&lt;/h3&gt;
&lt;p&gt;On Debian/Ubuntu with PM2, use SQLite’s built-in backup command (safe, works while the service is running):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;sqlite3&lt;/span&gt;&lt;span&gt; ~/page-stats/data/stats.db&lt;/span&gt;&lt;span&gt; &quot;.backup &apos;/home/YOUR_USERNAME/backups/stats-$(&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt; +%Y-%m-%d).db&apos;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;9) Troubleshooting&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;“Cannot GET /”&lt;/strong&gt; when visiting the VM IP: normal — the API only responds to &lt;code&gt;/track&lt;/code&gt;, &lt;code&gt;/summary&lt;/code&gt;, &lt;code&gt;/daily&lt;/code&gt;, and &lt;code&gt;/export&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mixed content blocked&lt;/strong&gt;: ensure the endpoint is &lt;strong&gt;HTTPS&lt;/strong&gt; and CORS allows your blog origin.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS check fails&lt;/strong&gt;: gray‑cloud the &lt;code&gt;stats&lt;/code&gt; record until the certificate is issued.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No data appears&lt;/strong&gt;: test with a direct &lt;code&gt;curl -X POST .../track&lt;/code&gt; and check &lt;code&gt;pm2 logs&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Test your endpoint manually&lt;/h3&gt;
&lt;p&gt;You can manually test your tracking endpoint with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; -X&lt;/span&gt;&lt;span&gt; POST&lt;/span&gt;&lt;span&gt; http://localhost:8080/track&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -H&lt;/span&gt;&lt;span&gt; &quot;Content-Type: text/plain&quot;&lt;/span&gt;&lt;span&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -d&lt;/span&gt;&lt;span&gt; &apos;{&quot;path&quot;:&quot;/hello&quot;,&quot;referrer&quot;:&quot;&quot;}&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; &quot;http://localhost:8080/summary&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new entry appearing in &lt;code&gt;/summary&lt;/code&gt; confirms your endpoint is working correctly.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This blog uses this as a &lt;strong&gt;self‑hosted, portable, privacy‑friendly analytics&lt;/strong&gt; system. If you don’t want me to know you’ve visited my blog (I’d be really sad 😢), you can simply use uBlock Origin to block the domain above. If you want to build your own, feel free to fork these snippets and adapt the endpoints to your domain.&lt;/p&gt;</content:encoded></item><item><title>An Inappropriate Metaphor: Misusing AI</title><link>https://michifumi.de/blog/2025-10-29-an-inappropriate-metaphor/</link><guid isPermaLink="true">https://michifumi.de/blog/2025-10-29-an-inappropriate-metaphor/</guid><description>On why misusing AI feels a lot like misusing a light bulb.</description><pubDate>Wed, 29 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;On why misusing AI feels a lot like misusing a light bulb.&lt;/em&gt;&lt;/p&gt;&lt;hr /&gt;&lt;p&gt;Many people claim AI tools are scams, AI is unreliable, and AI is riddled with hallucinations. I believe that when you encounter such issues, the problem most likely isn’t with the AI itself, but rather how you’re using it.&lt;/p&gt;
&lt;p&gt;Let’s take a light bulb as an example. A light bulb is designed to be screwed into a socket, powered on, and used to illuminate its surroundings. But if you insist on using it as a sex toy and shove it into your ass, you can certainly do that, however, you also run the risk of the bulb getting stuck. Because the bulb wasn’t designed for that purpose. It can do it, but it doesn’t do it well.&lt;/p&gt;
&lt;p&gt;Similarly, if you can’t articulate your own thoughts clearly, or worse, use incorrect phrasing to prompt AI responses, the AI will misinterpret your input and deliver inaccurate information. It can do, but not good, just like how you use the bulb.&lt;/p&gt;
&lt;p&gt;This is precisely why everyone should learn how to become a master AI prompt engineer, to use AI correctly, and make your life easier.&lt;/p&gt;
&lt;p&gt;You can start learning here.&lt;br /&gt;
&lt;a href=&quot;http://www.catb.org/~esr/faqs/smart-questions.html&quot;&gt;How To Ask Questions The Smart Way&lt;/a&gt;&lt;br /&gt;
Author: Eric Steven Raymond&lt;/p&gt;</content:encoded></item></channel></rss>