<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Frank&#39;s Blog</title>
    <link>https://frankblogs.com/</link>
    <description>Recent content on Frank&#39;s Blog</description>
    <generator>Hugo -- 0.150.0</generator>
    <language>en-us</language>
    <lastBuildDate>Fri, 10 Oct 2025 21:39:48 +0800</lastBuildDate>
    <atom:link href="https://frankblogs.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Service Deployment &amp; Advanced Troubleshooting on iStoreOS</title>
      <link>https://frankblogs.com/posts/article-6/</link>
      <pubDate>Fri, 10 Oct 2025 21:39:48 +0800</pubDate>
      <guid>https://frankblogs.com/posts/article-6/</guid>
      <description>With iStoreOS running stably on my N100 Proxmox setup, it was time to deploy AdGuard Home, Tailscale, and troubleshoot lingering issues like speed bottlenecks and remote access failures. This is where the real power of the platform shone.</description>
      <content:encoded><![CDATA[<p>My N100 soft router was finally running a stable router OS – iStoreOS – within a Proxmox VE virtual machine. The core networking was solid. Now came the rewarding part: deploying the services I actually wanted and tackling the inevitable advanced troubleshooting challenges.</p>
<h2 id="adguard-home-taming-the-dns-loop">AdGuard Home: Taming the DNS Loop</h2>
<p>Installing AdGuard Home (AGH) via the iStoreOS app store was straightforward. The initial setup wizard (<code>:3000</code>) guided me through setting ports. I changed AGH&rsquo;s DNS port to <code>:5353</code> to avoid conflict with iStoreOS&rsquo;s built-in <code>dnsmasq</code> (running on <code>:53</code>).</p>
<p>The goal: <code>Clients -&gt; iStoreOS (dnsmasq :53) -&gt; AdGuard Home (:5353) -&gt; Public DNS (e.g., 1.1.1.1)</code>.</p>
<p>Configuration involved:</p>
<ol>
<li><strong>dnsmasq:</strong> Set it to <em>not</em> forward upstream queries found in <code>/etc/resolv.conf</code>, and explicitly forward <em>all</em> queries to AGH listening on the loopback address: <code>server=127.0.0.1#5353</code>.</li>
<li><strong>AdGuard Home:</strong> Set its upstream DNS servers to Cloudflare/Google.</li>
<li><strong>DHCP:</strong> Configure iStoreOS&rsquo;s DHCP server (Option 6) to advertise the router&rsquo;s IP (<code>10.0.0.1</code>) as the DNS server for all clients.</li>
</ol>
<p>Initially, this resulted in DNS resolution failures and <code>context deadline exceeded</code> errors in the AGH logs. Diagnosis: a <strong>DNS loop</strong>. Dnsmasq was forwarding to AGH, but AGH, possibly due to a misconfiguration or startup order issue, was somehow trying to query dnsmasq back.</p>
<p>Adding to the confusion was a &ldquo;ghost configuration.&rdquo; I had initially tried installing AGH via a <code>curl</code> script before using the app store version. This seemed to leave conflicting settings. The breakthrough came almost accidentally: running the AdGuard Home binary directly from the command line with the <code>-s run</code> flag (<code>./AdGuardHome -s run</code>) triggered some kind of self-reset or reinitialization. After this, restarting the service properly allowed the correct DNS chain (<code>dnsmasq -&gt; AGH -&gt; Public</code>) to establish, and DNS resolution started working beautifully across my network.</p>
<h2 id="the-network-speed-crisis-a-virtualization-bottleneck">The Network Speed Crisis: A Virtualization Bottleneck</h2>
<p>With DNS filtering active, I ran a speed test. Disaster! My gigabit fiber connection, which previously delivered ~950 Mbps, was now capped at a measly <strong>400 Mbps</strong>.</p>
<p>The troubleshooting began:</p>
<ul>
<li><strong>Disable AdGuard Home:</strong> No change. DNS filtering wasn&rsquo;t the bottleneck.</li>
<li><strong>Check Physical Links:</strong> Used <code>ethtool eth0</code> and <code>ethtool eth1</code> within the iStoreOS VM to confirm the virtual NICs were negotiating at 2500 Mbps with the PVE bridges. Links were fine.</li>
<li><strong>Enable Software Flow Offloading:</strong> Toggled this common OpenWrt optimization. No significant change.</li>
</ul>
<p>The problem had to lie deeper, likely within the virtualization layer itself. Research pointed towards a common issue with high-speed networking in VMs: the CPU struggling to handle the sheer volume of network packets being processed by a single core for the virtual NIC.</p>
<h3 id="the-solution-multiqueue">The Solution: Multiqueue</h3>
<p>Proxmox VE (and the underlying KVM/QEMU) supports a feature called <strong>Multiqueue VirtIO-Net</strong>. This allows the network packet processing load for a virtual NIC to be spread across multiple vCPU cores assigned to the VM.</p>
<ol>
<li>Shut down the iStoreOS VM.</li>
<li>In the PVE web UI, go to the VM&rsquo;s &ldquo;Hardware&rdquo; tab.</li>
<li>Select the VirtIO network device corresponding to the <strong>LAN</strong> interface (<code>net0</code> in my case, connected to <code>vmbr1</code>).</li>
<li>Click &ldquo;Edit&rdquo;.</li>
<li>Check the <strong>&ldquo;Multiqueue&rdquo;</strong> box.</li>
<li>Set the &ldquo;Queues&rdquo; number equal to the number of vCPU cores assigned to the VM (e.g., if you gave it 4 cores, set Queues to 4).</li>
<li>Repeat steps 3-6 for the <strong>WAN</strong> network device (<code>net1</code>, connected to <code>vmbr0</code>).</li>
<li>Start the iStoreOS VM.</li>
</ol>
<p>Ran the speed test again. Success! Speeds jumped right back up to the ~950 Mbps range. The virtualization bottleneck was eliminated.</p>
<h2 id="tailscale-triumph-conquering-remote-access">Tailscale Triumph: Conquering Remote Access</h2>
<p>My final major goal was setting up remote access, primarily to manage services like my NAS (planned OMV VM) and potentially the router itself when away from home. I opted for <strong>Tailscale</strong> due to its ease of use and availability in the iStoreOS app store.</p>
<p>Installation was simple. Following the command-line instructions (<code>tailscale up --advertise-routes=10.0.0.0/24</code>) connected the router to my Tailnet and advertised my home LAN subnet. However, while the router appeared online in the Tailscale admin console, I couldn&rsquo;t access <em>any</em> devices on my home LAN (like <code>10.0.0.1</code>) from my phone connected via Tailscale.</p>
<h3 id="troubleshooting-round-1-firewall">Troubleshooting Round 1: Firewall</h3>
<p>Pinging <code>10.0.0.1</code> from my phone resulted in &ldquo;Destination Port Unreachable.&rdquo; This screamed <strong>firewall</strong>. iStoreOS hadn&rsquo;t automatically created a firewall zone or rules for the new <code>tailscale0</code> interface.</p>
<h3 id="troubleshooting-round-2-interface--zone">Troubleshooting Round 2: Interface &amp; Zone</h3>
<p>Following guidance from GitHub issues, I manually edited <code>/etc/config/network</code> to define the <code>tailscale0</code> interface:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">config interface <span class="s1">&#39;tailscale&#39;</span>
</span></span><span class="line"><span class="cl">        option proto <span class="s1">&#39;none&#39;</span>
</span></span><span class="line"><span class="cl">        option device <span class="s1">&#39;tailscale0&#39;</span>
</span></span></code></pre></div><p>Then, in the LuCI Firewall settings (<code>Network -&gt; Firewall</code>), I:</p>
<ol>
<li>Created a new firewall zone named <code>tailscale</code>.</li>
<li>Set its Input/Output/Forward policies to <code>ACCEPT</code>.</li>
<li>Assigned the <code>tailscale0</code> network interface to this zone.</li>
<li>Crucially, created a <strong>Forwarding rule</strong> allowing traffic from the <code>tailscale</code> zone to the <code>lan</code> zone.</li>
</ol>
<p>Saved and applied. Tried connecting again. The &ldquo;Port Unreachable&rdquo; error was gone, but now attempts to access <code>10.0.0.1</code>just&hellip; hung. Loading indefinitely.</p>
<h3 id="troubleshooting-round-3-mtumss">Troubleshooting Round 3: MTU/MSS</h3>
<p>This new symptom – indefinite loading – strongly suggested an <strong>MTU (Maximum Transmission Unit)</strong> mismatch problem, common with VPNs and tunnels like Tailscale. Packets were likely getting fragmented or dropped because their size exceeded what the tunnel could handle. The standard fix is <strong>MSS Clamping (Maximum Segment Size)</strong>, where the router rewrites TCP packet headers to request smaller segment sizes.</p>
<h3 id="the-final-fix-iptables-mss-clamping">The Final Fix: iptables MSS Clamping</h3>
<p>Added the following rules via <code>Network -&gt; Firewall -&gt; Custom Rules</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Tailscale MSS Clamping for IPv4</span>
</span></span><span class="line"><span class="cl">iptables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Tailscale MSS Clamping for IPv6 (if using IPv6 with Tailscale)</span>
</span></span><span class="line"><span class="cl">ip6tables -t mangle -A FORWARD -o tailscale0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
</span></span></code></pre></div><p>Restarted the firewall. Tried accessing <code>10.0.0.1</code> from my phone via Tailscale one last time.</p>
<p><strong>Victory!</strong> The iStoreOS login page loaded instantly. Remote access was finally conquered.</p>
<h2 id="conclusion-a-system-forged-in-debugging">Conclusion: A System Forged in Debugging</h2>
<p>This journey, from a simple IPv6 issue to a fully virtualized N100 soft router with custom services and robust remote access, was far more challenging than I initially anticipated. Every step seemed to uncover a new problem, requiring deep dives into networking concepts, Linux internals, and virtualization quirks.</p>
<p>However, each problem solved solidified my understanding and resulted in a final system that is incredibly powerful, stable, and perfectly tailored to my needs. The N100 platform with Proxmox and iStoreOS has proven to be an outstanding foundation. The debugging war is over, and the era of enjoying a truly capable home lab has begun.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Router VM Saga: OpenWrt vs. iStoreOS &amp; The Great Migration</title>
      <link>https://frankblogs.com/posts/article-5/</link>
      <pubDate>Mon, 06 Oct 2025 21:32:45 +0800</pubDate>
      <guid>https://frankblogs.com/posts/article-5/</guid>
      <description>With PVE running, it was time to create the router VM. This post details my struggles with vanilla OpenWrt, the dependency hell, and the eventual, crucial decision to migrate to the pre-integrated iStoreOS.</description>
      <content:encoded><![CDATA[<p>My Proxmox VE (PVE) hypervisor was installed and the host networking configured on my N100 box. Now it was time to create the core component: the virtual machine that would act as my main home router. My initial plan was to use vanilla <strong>OpenWrt</strong>, the classic, highly customizable Linux distribution for routers.</p>
<h2 id="act-i-the-struggles-with-vanilla-openwrt">Act I: The Struggles with Vanilla OpenWrt</h2>
<p>Creating the VM in PVE (giving it VM ID 100) involved several steps:</p>
<ol>
<li><strong>Download Firmware:</strong> Get the official OpenWrt x86-64 combined-ext4 image (<code>.img.gz</code>).</li>
<li><strong>Upload &amp; Import:</strong> Upload the <code>.img.gz</code> file to PVE (e.g., via SCP or the web UI&rsquo;s storage browser – note: uploading directly as an ISO image type fails). Import the disk image into the VM using <code>qm importdisk 100 openwrt.img.gz local-lvm</code>.</li>
<li><strong>VM Hardware Config:</strong> Assign CPU cores, RAM. Remove the default SCSI disk. Attach the imported OpenWrt disk as a VirtIO block device. Add two VirtIO network cards – one connected to PVE&rsquo;s <code>vmbr0</code> (WAN) and one to <code>vmbr1</code> (LAN). Set the boot order to use the OpenWrt disk.</li>
<li><strong>Disk Resizing:</strong> The default image is tiny. Resize the virtual disk in PVE&rsquo;s hardware tab. Boot the VM with a GParted Live ISO attached, use GParted to expand the OpenWrt partition to fill the larger virtual disk.</li>
<li><strong>Initial Network Config:</strong> Boot OpenWrt. Access the console (via PVE&rsquo;s web UI or <code>qm terminal 100</code> - not <code>qm console</code>). Use the <code>vi</code> editor to modify <code>/etc/config/network</code>, changing the LAN interface&rsquo;s IP address to <code>10.0.0.1</code>.</li>
</ol>
<p>So far, so good. I could access the OpenWrt LuCI web interface at <code>10.0.0.1</code>. I configured PPPoE on <code>eth1</code> (WAN, connected to <code>vmbr0</code>) and set up the LAN interface (<code>eth0</code>, connected to <code>vmbr1</code>) with DHCP and IPv6 server settings. Basic routing worked!</p>
<h3 id="the-dependency-hell">The Dependency Hell</h3>
<p>The problems began when I tried to customize OpenWrt by installing packages. My goal was simple: install <code>luci-app-adguardhome</code>. But <code>opkg update</code> often failed to connect to the repositories. When it did connect, attempting to install AdGuard Home resulted in a cascade of errors about <strong>incompatible architectures</strong> or missing dependencies (<code>luci-lua-runtime</code> was a frequent culprit). Even manually downloading <code>.ipk</code> packages failed with similar architecture errors.</p>
<p>I spent hours trying different package versions, compiling dependencies, and scouring forums. I even managed to &ldquo;force install&rdquo; some packages, but this led to a broken LuCI interface – the &ldquo;Services&rdquo; menu simply wouldn&rsquo;t appear, despite the package seemingly being installed.</p>
<p>[<strong>Placeholder: Add a screenshot of the terminal showing the <code>opkg</code> errors or the LuCI interface missing the Services menu.</strong>]</p>
<p>Clearing LuCI caches (<code>rm /tmp/luci-indexcache</code>) did nothing. Log files (<code>logread</code>) showed no obvious errors. File permissions seemed correct. The final nail in the coffin was a detailed error log clearly stating that key LuCI components and their dependencies were fundamentally incompatible with the base system. Trying to manually build a complex setup on vanilla OpenWrt felt like assembling a car from mismatched parts found in a junkyard.</p>
<h2 id="act-ii-the-turn-towards-istoreos">Act II: The Turn Towards iStoreOS</h2>
<p>Frustrated and realizing the &ldquo;DIY package management&rdquo; route was unsustainable, I revisited earlier advice: use a <strong>pre-integrated firmware</strong> like <strong>iStoreOS</strong>. Based on OpenWrt but comes with a user-friendly interface and, crucially, a curated app store where dependencies <em>should</em> just work.</p>
<h3 id="the-migration-plan">The Migration Plan</h3>
<p>The challenge: perform this migration without disrupting my family&rsquo;s internet access for too long.</p>
<ol>
<li><strong>Preparation:</strong> In PVE, create a <em>new</em> VM (e.g., VM ID 101) for iStoreOS. Download the iStoreOS x86 <code>.img.gz</code> firmware.</li>
<li><strong>Import &amp; Basic Config:</strong> Upload and import the iStoreOS disk into VM 101. Configure its hardware similarly to the OpenWrt VM (CPU, RAM, 2x VirtIO NICs connected to <code>vmbr0</code> and <code>vmbr1</code>).</li>
<li><strong>Offline Config Attempt (Failed):</strong> Boot VM 101 with GParted. Try to mount the iStoreOS partition to change the default IP (<code>192.168.100.1</code>) to <code>10.0.0.1</code> offline. <strong>Failure!</strong> GParted couldn&rsquo;t identify the filesystem. Manual <code>mount</code> commands failed, complaining about unknown filesystem types or missing superblocks. Offline config was impossible.</li>
<li><strong>The Switch:</strong> Plan B - configure online. Physically disconnect the WAN cable from the N100. Shut down the old OpenWrt VM (100). Start the new iStoreOS VM (101). Set my Mac&rsquo;s IP statically to <code>192.168.100.5</code>. Connect my Mac to one of the N100&rsquo;s LAN ports (connected to <code>vmbr1</code>). Try accessing <code>192.168.100.1</code>.</li>
</ol>
<h3 id="the-wanlan-inversion-twist">The WAN/LAN Inversion Twist</h3>
<p>Failure again! <code>192.168.100.1</code> was unreachable. In a moment of desperation (or maybe just random plugging), I moved my Mac&rsquo;s cable to the N100 port I had designated for <strong>WAN</strong> (connected to <code>vmbr0</code>). And&hellip; success! I reached the iStoreOS login page.</p>
<p><strong>Diagnosis:</strong> iStoreOS had, for some reason, <strong>reversed</strong> the network interface identification compared to OpenWrt. It saw <code>eth0</code> (connected to PVE&rsquo;s <code>vmbr1</code>, my LAN bridge) as its <em>WAN</em> interface, and <code>eth1</code> (connected to PVE&rsquo;s <code>vmbr0</code>, my WAN bridge) as its <em>LAN</em> interface, assigning the <code>192.168.100.1</code> address there.</p>
<p><strong>Resolution:</strong> Logged into iStoreOS via the &ldquo;wrong&rdquo; port. Navigated to <code>Network -&gt; Interfaces</code>. Edited the LAN and WAN interfaces, <strong>swapping</strong> their assigned physical devices (<code>eth0</code> &lt;-&gt; <code>eth1</code>). Saved and applied. Moved my Mac&rsquo;s cable back to a correct LAN port. Set my Mac back to DHCP. Accessed <code>192.168.100.1</code> (the default iStore IP). Success! Changed the iStoreOS LAN IP to <code>10.0.0.1</code>. Plugged the WAN cable back into the N100&rsquo;s WAN port.</p>
<p>The network migration was complete in under 15 minutes. The foundation was finally stable. iStoreOS provided the pre-packaged environment I needed. Now, it was time to deploy the services that started this whole journey.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Proxmox VE Initiation: Taming the Virtualization Hypervisor</title>
      <link>https://frankblogs.com/posts/article-4/</link>
      <pubDate>Wed, 01 Oct 2025 21:30:18 +0800</pubDate>
      <guid>https://frankblogs.com/posts/article-4/</guid>
      <description>With my N100 soft router hardware assembled, the next step was installing Proxmox VE (PVE). This post covers the installation process and my first major challenge: configuring PVE networking without losing access.</description>
      <content:encoded><![CDATA[<p>The N100 hardware for my new soft router/home server was assembled and ready. The grand vision involved virtualization, allowing this single machine to run my router, NAS, and potentially other services simultaneously. The chosen foundation for this was <strong>Proxmox Virtual Environment (PVE)</strong>, a powerful, open-source hypervisor based on Debian Linux.</p>
<h2 id="installing-pve-the-first-hurdle">Installing PVE: The First Hurdle</h2>
<p>Installing PVE itself is relatively straightforward, similar to installing any Linux distribution:</p>
<ol>
<li><strong>Create Installation Media:</strong> Download the PVE ISO image. On my Mac, I used <strong>Balena Etcher</strong> to write the ISO to a USB drive. My first attempt failed – turns out the old USB drive I grabbed was faulty. A newer drive worked perfectly.</li>
<li><strong>Boot from USB:</strong> Connect a monitor and keyboard to the N100 box, plug in the USB drive, and boot into the BIOS/UEFI settings (usually by pressing DEL or F2 during startup). Set the USB drive as the primary boot device.</li>
<li><strong>PVE Installer:</strong> Follow the graphical installer prompts. Key settings during installation:
<ul>
<li>Target Hard Disk: Select the Fanxiang NVMe SSD.</li>
<li>Location/Timezone: Set appropriately.</li>
<li>Hostname: Set to something memorable, like <code>pve.local</code>.</li>
<li>Network Configuration: <strong>Crucially</strong>, assign a <strong>static IP address</strong> for PVE&rsquo;s management interface (e.g., <code>10.0.0.2</code>), along with the gateway (<code>10.0.0.1</code> - my eventual router VM&rsquo;s address) and DNS servers. I chose one of the four physical ports (e.g., <code>enp1s0</code>) for this initial management access.</li>
</ul>
</li>
</ol>
<p>The installation completed smoothly, and PVE rebooted. I could access the command line directly on the N100, and more importantly, access the web UI from my Mac at <code>https://10.0.0.2:8006</code>. Stage one complete!</p>
<h2 id="the-networking-nightmare-reconfiguring-for-vms">The Networking Nightmare: Reconfiguring for VMs</h2>
<p>PVE&rsquo;s default network setup uses one physical port (<code>enp1s0</code> in my case) solely for its own management. However, to allow my <em>virtual machines</em> (like the router VM) to connect to both the internet (WAN) and my home network (LAN), I needed to create <strong>Linux Bridges</strong>.</p>
<p>My plan:</p>
<ul>
<li><code>vmbr0</code>: Use a different physical port (e.g., <code>enp2s0</code>) for the <strong>WAN</strong> connection (connecting to my ISP modem).</li>
<li><code>vmbr1</code>: Use the remaining two physical ports (e.g., <code>enp3s0</code>, <code>enp4s0</code>) bridged together for the <strong>LAN</strong> connection (connecting to my home switch).</li>
<li><strong>Crucially:</strong> Move PVE&rsquo;s <em>own</em> management interface from the physical port <code>enp1s0</code> onto the newly created <strong>LAN bridge (<code>vmbr1</code>)</strong>. This way, I could manage PVE from any computer on my home network using the <code>10.0.0.2</code> address, freeing up <code>enp1s0</code> perhaps for other uses.</li>
</ul>
<p>This reconfiguration is done by editing the <code>/etc/network/interfaces</code> file directly via the PVE console (or SSH). I carefully modified the file, creating the bridge definitions and changing the management interface settings.</p>
<p>I applied the changes (<code>systemctl restart networking</code> or a reboot). And then&hellip; disaster.</p>
<p>I could no longer access the PVE web UI at <code>10.0.0.2</code>. Pings failed. It seemed I had completely locked myself out.</p>
<h2 id="troubleshooting-in-the-dark">Troubleshooting in the Dark</h2>
<p>Panic set in. Had I made a typo in the config file? Did the bridge fail to come up? I had to go back to the &ldquo;direct connection&rdquo; method: plug a monitor and keyboard directly into the N100 box.</p>
<p>Logging into the PVE console, I ran <code>ip a</code> to check the network interface status. The output revealed the problem: <code>vmbr1</code> (my intended LAN bridge and new management interface) <strong>had no IP address</strong>. My configuration changes hadn&rsquo;t been applied correctly, or something had gone wrong during the network restart.</p>
<p>Carefully re-examining <code>/etc/network/interfaces</code> line by line, I eventually spotted the error – perhaps a typo, a missing <code>auto vmbr1</code>, or incorrect bridge port settings. I corrected the file using the <code>nano</code> editor (a bit friendlier than <code>vi</code> for quick edits).</p>
<p>After saving the corrected file and restarting the networking service again (<code>systemctl restart networking</code>), I ran <code>ip a</code> one more time. Success! <code>vmbr1</code> now showed the correct <code>10.0.0.2</code> address.</p>
<p>Back on my Mac, I refreshed <code>https://10.0.0.2:8006</code>, and the Proxmox login screen reappeared. Access restored!</p>
<p>This harrowing experience was my first real taste of configuring Linux networking and the importance of meticulousness when editing critical system files. With the PVE host networking finally stable, I was ready for the main event: creating the virtual machine that would become my new home router.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Entering the World of x86 Soft Routers: Choosing the N100 Beast</title>
      <link>https://frankblogs.com/posts/article-3/</link>
      <pubDate>Thu, 25 Sep 2025 21:21:37 +0800</pubDate>
      <guid>https://frankblogs.com/posts/article-3/</guid>
      <description>After failed attempts with consumer routers, I decided to take the plunge into the world of x86 soft routers. This post covers the decision process, comparing platforms like N5105 and N100, and my final hardware selection.</description>
      <content:encoded><![CDATA[<p>My journey to achieve reliable home networking, particularly stable IPv6, had hit roadblocks with both my ISP&rsquo;s modem and a powerful-but-faulty Netgear R8000 running OpenWrt. It became clear that consumer-grade hardware, even with custom firmware, might not offer the raw power, flexibility, and reliability I craved. It was time to explore the next level: <strong>x86 Soft Routers</strong>.</p>
<h2 id="what-is-a-soft-router">What is a Soft Router?</h2>
<p>Unlike traditional &ldquo;hard&rdquo; routers with proprietary hardware and software, a soft router is essentially a small, dedicated PC running a specialized router operating system (like OpenWrt, pfSense, OPNsense, or RouterOS). The &ldquo;x86&rdquo; refers to the standard PC processor architecture (Intel/AMD), offering significantly more power and flexibility than the ARM or MIPS chips found in most consumer routers.</p>
<h2 id="the-allure-of-x86">The Allure of x86</h2>
<p>The potential benefits were immense:</p>
<ul>
<li><strong>Raw Performance:</strong> Capable of handling gigabit+ speeds with complex firewall rules, VPNs, and traffic shaping without breaking a sweat.</li>
<li><strong>Flexibility:</strong> Run virtually any router OS or even standard Linux distributions.</li>
<li><strong>Expandability:</strong> Add more RAM, faster storage, or even specialized network cards.</li>
<li><strong>Virtualization:</strong> This was the game-changer. An x86 platform could potentially run <em>multiple</em> operating systems simultaneously using a hypervisor like Proxmox VE (PVE). I could have my router, a NAS, a media server, and more, all running on one efficient box.</li>
</ul>
<h2 id="choosing-the-platform-n5105j4125-vs-n100">Choosing the Platform: N5105/J4125 vs. N100</h2>
<p>My research focused on popular low-power x86 chips commonly used in mini PCs and soft routers:</p>
<ul>
<li><strong>Older Generation (Jasper Lake N5105, Celeron J4125):</strong> Widely available, often cheaper, proven platforms. Decent performance for routing and light server tasks.</li>
<li><strong>New Generation (Alder Lake-N N100):</strong> Significant performance uplift (especially single-core, crucial for routing), much better integrated graphics (useful for PVE console/light desktop tasks), generally more power-efficient, supports faster DDR5 RAM. Often comes paired with newer <strong>Intel i226-V 2.5GbE</strong> network controllers, known for better driver support and stability compared to the sometimes-problematic Realtek chips found in cheaper units.</li>
</ul>
<p>Given the relatively small price difference for new units and the significant performance and efficiency gains, the <strong>Intel N100</strong> quickly became the clear winner for future-proofing.</p>
<h2 id="navigating-the-hardware-maze">Navigating the Hardware Maze</h2>
<p>Choosing the N100 platform opened up a dizzying array of options, mostly from Chinese manufacturers like Beikong (Topton/Kingnovy), CWWK, etc., often sold as barebones systems (no RAM/SSD). Key considerations:</p>
<ul>
<li><strong>Ports:</strong> How many? What speed? The standard was 4x 2.5GbE Intel i226-V ports, perfect for high-speed LAN and future ISP upgrades. Some offered 2x 2.5GbE + 2x 10GbE SFP+ (fiber) – tempting, but overkill and more expensive for my current needs.</li>
<li><strong>Cooling:</strong> Passive (fanless) is preferred for silent operation, requiring a well-designed chassis.</li>
<li><strong>RAM/SSD:</strong> DDR5 SO-DIMM and NVMe M.2 slots were standard.</li>
<li><strong>Price:</strong> Barebones N100 boxes with 4x 2.5GbE ports were readily available.</li>
<li><strong>Used Market:</strong> While tempting for cost savings, diagnosing potential issues on used hardware felt risky after my R8000 experience.</li>
</ul>
<h2 id="my-final-choice-beikong-h30w-n100">My Final Choice: Beikong H30W (N100)</h2>
<p>After much comparison, I settled on a brand new <strong>Beikong H30W N100 barebones</strong> unit featuring:</p>
<ul>
<li>Intel N100 Processor</li>
<li>4x Intel i226-V 2.5GbE ports</li>
<li>Passive cooling chassis</li>
<li>DDR5 SO-DIMM slot</li>
<li>NVMe M.2 slot</li>
</ul>
<p>To complete the build with maximum cost-effectiveness:</p>
<ul>
<li><strong>RAM:</strong> I salvaged a compatible 16GB DDR5 stick from an old laptop – a huge saving!</li>
<li><strong>SSD:</strong> I purchased a new, budget-friendly but well-regarded <strong>Fanxiang S500 Pro 256GB NVMe SSD</strong>. Plenty of space for PVE and multiple VMs.</li>
</ul>
<h2 id="the-grand-vision-virtualization">The Grand Vision: Virtualization</h2>
<p>With the hardware assembled, the real excitement began. This N100 wasn&rsquo;t just going to be a router. It was going to be the heart of my home lab. The plan: install <strong>Proxmox VE (PVE)</strong> as the bare-metal hypervisor, then create virtual machines for:</p>
<ol>
<li><strong>OpenWrt/iStoreOS:</strong> My primary router, firewall, and network manager.</li>
<li><strong>OpenMediaVault (OMV):</strong> A NAS for file storage and backups.</li>
<li><strong>Future Possibilities:</strong> Home Assistant, AdGuard Home, a web server, maybe even AI experiments?</li>
</ol>
<p>This N100 box represented a convergence of all my needs and aspirations. The hardware was ready. Now came the software challenge: installing and taming the Proxmox hypervisor.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Failed Rescue of a Bricked Router: An OpenWrt Adventure</title>
      <link>https://frankblogs.com/posts/article-2/</link>
      <pubDate>Sat, 20 Sep 2025 20:59:17 +0800</pubDate>
      <guid>https://frankblogs.com/posts/article-2/</guid>
      <description>With my ISP modem failing to provide proper IPv6, I turned to an old, supposedly bricked Netgear R8000 running OpenWrt. This post details the intense process of reviving it via Failsafe Mode, only to hit another dead end.</description>
      <content:encoded><![CDATA[<p>My quest for functional IPv6 hit a wall with the ISP&rsquo;s faulty modem (ONT). Since I couldn&rsquo;t get a proper <strong>Prefix Delegation (PD)</strong> from it, I needed a router capable enough to potentially handle the connection differently, perhaps even taking over PPPoE duties if necessary. My gaze fell upon a Netgear R8000 Nighthawk X6 collecting dust – a once-powerful router I had previously flashed with OpenWrt, only to seemingly &ldquo;brick&rdquo; it during a configuration mishap. Could this be its moment of redemption?</p>
<h2 id="operation-unbrick">Operation: Unbrick</h2>
<p>Reviving a bricked OpenWrt router usually involves entering <strong>Failsafe Mode</strong>, a minimal environment allowing command-line access. The process is timing-sensitive and requires precision:</p>
<ol>
<li>
<p><strong>Static IP Setup:</strong> I configured my computer with a static IP address in the <code>192.168.1.x</code> range (e.g., <code>192.168.1.2</code>), subnet mask <code>255.255.255.0</code>.</p>
</li>
<li>
<p><strong>Direct Connection:</strong> Connected the computer directly to one of the R8000&rsquo;s LAN ports.</p>
</li>
<li>
<p><strong>Power Cycle &amp; Trigger:</strong> Powered on the R8000. As soon as a specific LED (usually the Power or WPS light) started blinking rapidly, I repeatedly pressed the WPS button. This triggers Failsafe Mode.</p>
</li>
<li>
<p><strong>SSH Connection:</strong> Opened a terminal and attempted to SSH into the router&rsquo;s default failsafe IP:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh root@192.168.1.1
</span></span></code></pre></div></li>
</ol>
<p>After a few tense tries, catching the blinking light at just the right moment, I got a connection! The familiar OpenWrt command prompt appeared. From there, the standard unbricking procedure followed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Mount the root filesystem read-write</span>
</span></span><span class="line"><span class="cl">mount_root
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Reset configuration to defaults</span>
</span></span><span class="line"><span class="cl">firstboot
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Force reboot</span>
</span></span><span class="line"><span class="cl">reboot -f
</span></span></code></pre></div><h2 id="the-mysterious-10001">The Mysterious 10.0.0.1</h2>
<p>After the reboot, I expected to access the OpenWrt LuCI web interface at the standard <code>192.168.1.1</code>. But&hellip; nothing. The page wouldn&rsquo;t load. Puzzled, I opened the terminal again and ran <code>ping 192.168.1.1</code>. No response.</p>
<p>What was going on? I vaguely remembered the OpenWrt firmware I had flashed wasn&rsquo;t an official build, but a custom one from a forum. Could it have a different default IP? I decided to check my computer&rsquo;s network settings – had it received an IP via DHCP?</p>
<p>Indeed, it had received <code>10.0.0.141</code>. The gateway? <code>10.0.0.1</code>.</p>
<p>A quick <code>ping 10.0.0.1</code> confirmed it – the router was alive and well, just hiding at a non-standard address! This custom firmware developer had decided to use <code>10.0.0.1</code> as the default management IP. Lesson learned: always check DHCP when standard IPs fail.</p>
<h2 id="configuring-the-r8000-hope-rises">Configuring the R8000 (Hope Rises)</h2>
<p>Logging into the LuCI interface at <code>10.0.0.1</code>, I proceeded to configure the R8000 as my main router:</p>
<ul>
<li><strong>PPPoE:</strong> Set up the WAN interface for PPPoE dialing using my ISP credentials. Success – it connected and got an IPv4 address (still <strong>CGNAT</strong>, of course).</li>
<li><strong>IPv6:</strong> Configured the WAN6 interface for &ldquo;Native IPv6&rdquo;. As expected, due to the ONT issue, it didn&rsquo;t get a delegated prefix, but it did acquire a WAN IPv6 address.</li>
<li><strong>Interfaces:</strong> Mapped the physical ports correctly (identifying <code>eth0.2</code> as WAN and <code>eth0.1</code>/<code>br-lan</code> as LAN).</li>
<li><strong>Wi-Fi:</strong> Configured the SSIDs and security. Interestingly, I initially set WPA3, but some older devices failed to connect. Downgrading to WPA2-PSK resolved this compatibility issue. Set appropriate channel widths (20MHz for 2.4GHz to reduce interference, 80MHz for 5GHz for speed).</li>
</ul>
<p>For a moment, things looked promising. The router was routing traffic, Wi-Fi was working (albeit with some WPA3 issues). Could this be the solution?</p>
<h2 id="the-final-failure-hope-dashed">The Final Failure (Hope Dashed)</h2>
<p>My optimism was short-lived. As I started exploring further configuration – trying to set up firewall rules or install necessary packages (<code>opkg update</code> often failed or was incredibly slow) – the R8000 started acting erratically. Sometimes the interface would become unresponsive. Other times, the lights would start flashing indefinitely, requiring a hard reboot.</p>
<p>After hours of troubleshooting, observing the inconsistent behavior, I had to face the harsh reality: this router wasn&rsquo;t just bricked by configuration before; it likely had an underlying, <strong>unrecoverable hardware fault</strong>. The flash memory was probably damaged, leading to corrupted data and instability under load.</p>
<p>The rescue attempt had failed. The Netgear R8000 was truly destined for the electronics recycling bin.</p>
<p>It seemed consumer-grade hardware, even powerful ones running flexible firmware like OpenWrt, had reached its limit for my needs. If I wanted reliability, performance, and the ability to truly overcome my network&rsquo;s challenges, I needed to look towards a different class of hardware altogether: the world of x86 soft routers.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The IPv6 Nightmare and the Double NAT Trap: My Quest for True Connectivity</title>
      <link>https://frankblogs.com/posts/article-1/</link>
      <pubDate>Thu, 18 Sep 2025 19:57:57 +0800</pubDate>
      <guid>https://frankblogs.com/posts/article-1/</guid>
      <description>My journey began with a simple problem: my ISP router wouldn&amp;#39;t give my devices public IPv6 addresses. Little did I know it would lead me down a rabbit hole of Prefix Delegation failures, Double NAT, and CGNAT.</description>
      <content:encoded><![CDATA[<p>It all started with a seemingly simple problem. My home network, centered around a Xiaomi AX6000 router running its stock firmware, was supposed to support IPv6. I had enabled it, and indeed, the router&rsquo;s WAN port proudly displayed a public IPv6 address. Yet, none of my devices – my computers, my phone, my game consoles – could get one. Accessing IPv6-only websites was impossible.</p>
<h2 id="diving-deep-into-ipv6-and-network-structure">Diving Deep into IPv6 and Network Structure</h2>
<p>After some initial head-scratching, research pointed towards a concept called <strong>Prefix Delegation (PD)</strong>. In simple terms, my ISP assigns a block (prefix) of IPv6 addresses to my router, and the router is then responsible for distributing individual addresses from that block to my devices. The symptom – WAN gets an address, LAN doesn&rsquo;t – strongly suggested PD was failing.</p>
<p>Why? The prime suspect became my <strong>ISP&rsquo;s modem (Optical Network Terminal or ONT)</strong>. Was it running in router mode instead of bridge mode? If the modem itself was acting as a router and didn&rsquo;t properly support delegating the prefix downstream, my AX6000 would never receive the necessary block.</p>
<p>I confirmed my network topology: ONT (in router mode) -&gt; Switch -&gt; Multiple Routers (including the AX6000). Unfortunately, due to other devices directly connected to the ONT via the switch, simply switching the ONT to bridge mode wasn&rsquo;t feasible without restructuring everything.</p>
<p>As a temporary workaround, I enabled <strong>NAT6</strong> on the AX6000. Success! My devices could now access IPv6 websites. However, I knew this wasn&rsquo;t <em>true</em> end-to-end IPv6. NAT6 essentially translates internal IPv6 addresses, similar to how traditional NAT works for IPv4. It solved the immediate browsing issue but didn&rsquo;t address the underlying PD failure and wouldn&rsquo;t help with services requiring direct public IPv6 reachability.</p>
<h2 id="enter-the-double-nat-monster">Enter the Double NAT Monster</h2>
<p>Around the same time, my Xbox started complaining: &ldquo;Double NAT detected.&rdquo; This made perfect sense. My network traffic was going through two layers of Network Address Translation: first at the AX6000, and then again at the ISP&rsquo;s ONT (which was also acting as a router). This configuration is notorious for causing issues with online gaming, peer-to-peer connections, and services requiring open ports.</p>
<p>We discussed potential mitigations like UPnP, manual port forwarding, and placing the AX6000 in the ONT&rsquo;s DMZ, but none felt like a clean solution.</p>
<h2 id="the-cgnat-revelation">The CGNAT Revelation</h2>
<p>The real blow came when investigating the Double NAT further. By comparing the WAN IP address shown on my ONT&rsquo;s status page with the public IP address reported by websites like &ldquo;whatismyip.com&rdquo;, I confirmed a dreaded reality: I was behind <strong>Carrier-Grade NAT (CGNAT)</strong>. My ISP wasn&rsquo;t assigning my ONT a unique, public IPv4 address. Instead, I was sharing one with potentially hundreds of other customers.</p>
<p>[<strong>Placeholder: Add a screenshot here showing the discrepancy between your ONT&rsquo;s WAN IP (likely a private 10.x or 100.x address) and your public IP, confirming CGNAT.</strong>]</p>
<p>This revelation made two things crystal clear:</p>
<ol>
<li>Solving the Xbox NAT issue reliably via IPv4 was going to be nearly impossible without resorting to complex (and often unreliable) tunneling solutions.</li>
<li>Getting <strong>native, public IPv6</strong> working correctly was no longer just desirable; it was <strong>essential</strong> for any kind of future-proof home networking or self-hosting.</li>
</ol>
<h2 id="back-to-the-ipv6-root-the-faulty-ont">Back to the IPv6 Root: The Faulty ONT</h2>
<p>With renewed focus, the investigation circled back to the ONT. I confirmed the ONT itself <em>was</em> receiving a <code>/64</code> or similar public IPv6 prefix starting with <code>240e:</code>. The problem was solely its inability to delegate this prefix downstream.</p>
<p>Analyzing the ONT&rsquo;s configuration page revealed a peculiar &ldquo;Port Binding&rdquo; setting. Initially, only LAN1 was bound for internet services.</p>
<p>I modified the settings to bind <em>all</em> LAN ports. Hope surged&hellip; but was quickly dashed. Even after ensuring all ports were bound, testing with the AX6000 and other routers on different ports yielded the same result: no PD, no LAN-side public IPv6.</p>
<p>The conclusion was unavoidable: the ONT&rsquo;s Prefix Delegation functionality was faulty, likely due to a hardware limitation or firmware bug. Relying on it was a dead end.</p>
<p>I needed a router powerful and flexible enough to potentially work around the ONT&rsquo;s limitations, or perhaps even replace its routing functions altogether. This led me to revisit an old piece of hardware sitting in my closet&hellip; a supposedly bricked Netgear R8000 running OpenWrt. Could it be resurrected?</p>
]]></content:encoded>
    </item>
    <item>
      <title>The About Page Is Back</title>
      <link>https://frankblogs.com/posts/the-about-page-is-back/</link>
      <pubDate>Sun, 14 Sep 2025 21:12:48 +0800</pubDate>
      <guid>https://frankblogs.com/posts/the-about-page-is-back/</guid>
      <description>&lt;p&gt;In my last post, I declared that I had killed my &amp;ldquo;About&amp;rdquo; page. It was a pragmatic decision. The page was a ghost: it rendered as raw HTML, all its links pointed to &lt;code&gt;localhost:1313&lt;/code&gt;, and it pulled in a phantom Chinese site title that didn&amp;rsquo;t exist in my configuration.&lt;/p&gt;
&lt;p&gt;Then, immediately after publishing that post, I discovered my &amp;ldquo;Tags&amp;rdquo; page was broken in the exact same way.&lt;/p&gt;
&lt;p&gt;When I clicked the &amp;ldquo;Tags&amp;rdquo; link in my (perfectly functional) menu, it loaded a page that &lt;em&gt;also&lt;/em&gt; had no CSS and contained only a giant &amp;ldquo;#Tags&amp;rdquo; title, with no actual tags listed—even though my post was correctly tagged and published.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>In my last post, I declared that I had killed my &ldquo;About&rdquo; page. It was a pragmatic decision. The page was a ghost: it rendered as raw HTML, all its links pointed to <code>localhost:1313</code>, and it pulled in a phantom Chinese site title that didn&rsquo;t exist in my configuration.</p>
<p>Then, immediately after publishing that post, I discovered my &ldquo;Tags&rdquo; page was broken in the exact same way.</p>
<p>When I clicked the &ldquo;Tags&rdquo; link in my (perfectly functional) menu, it loaded a page that <em>also</em> had no CSS and contained only a giant &ldquo;#Tags&rdquo; title, with no actual tags listed—even though my post was correctly tagged and published.</p>
<p>This was the final, definitive clue.</p>
<h2 id="the-unified-bug-theory">The Unified Bug Theory</h2>
<p>This confirmed the problem wasn&rsquo;t a series of isolated glitches. It was one, single, catastrophic bug. My chosen theme, <code>hugo-paper</code>, had a fatal flaw.</p>
<p>Its &ldquo;list&rdquo; templates (like the homepage) worked perfectly.
But its &ldquo;single&rdquo; template (for the About page) and its &ldquo;terms&rdquo; template (for the Tags page) were both fundamentally broken. They contained code that, for some reason, failed only in the Cloudflare Pages production environment, defaulting all asset paths to <code>localhost</code>.</p>
<p>My local build (<code>hugo server -D</code>) worked perfectly, which had masked this problem for days and led me down a debugging rabbit hole of config files, environment variables, and build flags. None of those fixes worked, because the theme&rsquo;s engine itself was defective.</p>
<h2 id="the-theme-transplant">The Theme Transplant</h2>
<p>The solution was obvious. You don&rsquo;t fix a broken engine; you replace it.</p>
<p>I decided to perform a &ldquo;theme transplant.&rdquo; I ripped out <code>hugo-paper</code> completely. This involved:</p>
<ol>
<li>Running <code>git submodule deinit</code> and <code>git rm</code> to purge the old theme from my project.</li>
<li>Choosing a new, robust, and extremely popular theme: <strong>hugo-PaperMod</strong>.</li>
<li>Adding it as a new submodule: <code>git submodule add ... themes/PaperMod</code>.</li>
</ol>
<p>Of course, this came with its own final hurdles. My brand new Hugo version (v0.150.0) immediately threw an error because the new theme&rsquo;s config used a deprecated <code>paginate</code> key. After fixing that (it&rsquo;s now <code>[pagination].pagerSize</code>), the Tags page <em>still</em> showed up empty.</p>
<p>One final search revealed the truth: in our quest to create a &ldquo;clean&rdquo; config, we had removed the block that <em>activates</em> taxonomies. The final piece of the puzzle was adding this back to <code>hugo.toml</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">taxonomies</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">tag</span> <span class="p">=</span> <span class="s2">&#34;tags&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">category</span> <span class="p">=</span> <span class="s2">&#34;categories&#34;</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    <item>
      <title>About Me</title>
      <link>https://frankblogs.com/about/</link>
      <pubDate>Sun, 14 Sep 2025 21:05:32 +0800</pubDate>
      <guid>https://frankblogs.com/about/</guid>
      <description>&lt;p&gt;Welcome to my corner of the internet!&lt;/p&gt;
&lt;p&gt;My name is Frank, and I&amp;rsquo;m currently navigating high school while exploring my deep fascination with technology, problem-solving, and the intricate process of building things – both digital and sometimes physical.&lt;/p&gt;
&lt;p&gt;This blog serves as my personal notebook and digital playground. It&amp;rsquo;s where I plan to document my journey as I dive into coding projects, experiment with web development (like the creation and ongoing refinement of this site itself!), tackle interesting technical challenges I encounter, and share insights or concepts I&amp;rsquo;m learning along the way. You might also find occasional posts about gaming or other topics that capture my interest.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Welcome to my corner of the internet!</p>
<p>My name is Frank, and I&rsquo;m currently navigating high school while exploring my deep fascination with technology, problem-solving, and the intricate process of building things – both digital and sometimes physical.</p>
<p>This blog serves as my personal notebook and digital playground. It&rsquo;s where I plan to document my journey as I dive into coding projects, experiment with web development (like the creation and ongoing refinement of this site itself!), tackle interesting technical challenges I encounter, and share insights or concepts I&rsquo;m learning along the way. You might also find occasional posts about gaming or other topics that capture my interest.</p>
<p>Building this website using Hugo, deploying it globally via Cloudflare Pages, and integrating features like search, comments with Giscus, and even LaTeX rendering has been an incredible learning adventure in itself. It pushed me to understand web infrastructure, Git workflows (CI/CD), and the realities of debugging in a way no textbook could.</p>
<p>Here, you&rsquo;ll find posts reflecting that journey: projects I&rsquo;m working on, interesting tech I&rsquo;m exploring, perhaps breakdowns of problems I&rsquo;ve solved (or spectacularly failed to solve!), and things I simply find cool.</p>
<p>My hope is that you&rsquo;ll find something interesting, useful, or perhaps even relatable in these digital pages.</p>
<p>Feel free to explore using the navigation or the search function. Thanks for stopping by!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Why I Killed My About Page</title>
      <link>https://frankblogs.com/posts/why-i-killed-my-about-page/</link>
      <pubDate>Sun, 14 Sep 2025 19:58:08 +0800</pubDate>
      <guid>https://frankblogs.com/posts/why-i-killed-my-about-page/</guid>
      <description>&lt;p&gt;This is my first post. It was supposed to be my second.&lt;/p&gt;
&lt;p&gt;My very first post should have been the &amp;ldquo;About&amp;rdquo; page. It’s the standard, simple, “Hello, this is who I am” page that every blog is supposed to have.&lt;/p&gt;
&lt;p&gt;But I deleted it. I killed it before this blog ever saw the light of day.&lt;/p&gt;
&lt;p&gt;This isn’t a great start, but it was a necessary one.&lt;/p&gt;
&lt;h2 id=&#34;this-was-supposed-to-be-easy&#34;&gt;This Was Supposed to Be Easy&lt;/h2&gt;
&lt;p&gt;My goal was simple: set up a personal blog using the &amp;ldquo;golden stack&amp;rdquo; of modern static sites: Hugo (as the engine), the &lt;code&gt;hugo-paper&lt;/code&gt; theme (for the clean look), Git, and Cloudflare Pages (for fast, free, global hosting).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>This is my first post. It was supposed to be my second.</p>
<p>My very first post should have been the &ldquo;About&rdquo; page. It’s the standard, simple, “Hello, this is who I am” page that every blog is supposed to have.</p>
<p>But I deleted it. I killed it before this blog ever saw the light of day.</p>
<p>This isn’t a great start, but it was a necessary one.</p>
<h2 id="this-was-supposed-to-be-easy">This Was Supposed to Be Easy</h2>
<p>My goal was simple: set up a personal blog using the &ldquo;golden stack&rdquo; of modern static sites: Hugo (as the engine), the <code>hugo-paper</code> theme (for the clean look), Git, and Cloudflare Pages (for fast, free, global hosting).</p>
<p>And to be honest, it <em>worked</em>. Mostly.</p>
<p>Except for one, single, impossibly stubborn page.</p>
<h2 id="the-ghost-in-the-machine">The Ghost in the Machine</h2>
<p>After I pushed my code, a bizarre problem appeared. My homepage loaded perfectly. The <code>/posts/</code> list page loaded perfectly. They had the correct styles (CSS) and the correct English site title (&ldquo;Frank&rsquo;s Blog&rdquo;).</p>
<p>But the <code>/about/</code> page was a ghost.</p>
<p>It rendered as raw, unstyled HTML. Worse, every single link on that one page (the CSS file, the link back to the homepage, the footer links) was broken, pointing to an impossible address: <code>http://localhost:1313/</code>.</p>
<p>To make it even weirder, the footer—which was supposed to pull my site&rsquo;s English title—instead showed an old, non-existent <strong>Chinese</strong> title: &ldquo;Frank 的博客&rdquo;.</p>
<p>My site was having a split-personality crisis. The homepage knew it was an English site deployed to production, while the About page thought it was a Chinese site running on a local test server.</p>
<h2 id="the-debugging-rabbit-hole">The Debugging Rabbit Hole</h2>
<p>So began an epic debugging saga. An AI assistant and I went down every conceivable rabbit hole to fix this. We tried:</p>
<ol>
<li><strong>Fixing the Config File?</strong> We triple-checked <code>hugo.toml</code>. We fixed deprecated keys (<code>paginate</code>), corrected TOML syntax, and scoured the file for invisible whitespace characters. <strong>The problem remained.</strong></li>
<li><strong>A &ldquo;Ghost&rdquo; Config?</strong> We hypothesized a &ldquo;ghost&rdquo; multilingual file (<code>languages.toml</code>) left over from the theme&rsquo;s example site was forcing the build into a broken state. We hunted it down and deleted the entire <code>config</code> directory. <strong>The problem remained.</strong></li>
<li><strong>Build Environment Mismatch?</strong> The most maddening clue was that <code>hugo server -D</code> on my local Mac worked <strong>perfectly</strong>. The About page was beautiful. This suggested a conflict with Cloudflare&rsquo;s build environment. So, we set the <code>HUGO_VERSION</code> environment variable in Cloudflare to match my exact local version. <strong>The problem remained.</strong></li>
<li><strong>A Build Command Override?</strong> We thought maybe Cloudflare was ignoring the <code>baseURL</code> in the config. So we manually forced it in the build command: <code>hugo --baseURL https://frankblogs.com/</code>. <strong>The problem remained.</strong></li>
<li><strong>A File Structure Problem?</strong> Our final theory. Maybe the theme&rsquo;s &ldquo;single page&rdquo; template was buggy, but its &ldquo;list page&rdquo; template (used by the working homepage) was fine. We changed the file structure from <code>content/about.md</code> to <code>content/about/_index.md</code> to &ldquo;trick&rdquo; Hugo into using the working template. <strong>And still, it failed.</strong></li>
</ol>
<h2 id="the-verdict">The Verdict</h2>
<p>After trying every logical configuration fix, the conclusion was inescapable.</p>
<p>The problem wasn&rsquo;t my config. It wasn&rsquo;t Cloudflare. It was a deep, unfixable bug in the <code>hugo-paper</code> theme itself—a flaw in its single-page template that, for some reason, fails catastrophically <em>only</em> when built on the Cloudflare Pages environment, defaulting all its paths to <code>localhost</code>. It was invisible locally, but fatal in production.</p>
<p>So I had a choice: spend the next week learning the Go Template language to debug a third-party theme I didn&rsquo;t write&hellip; or just delete the broken feature.</p>
<p>I chose deletion.</p>
<p>This blog is going to be a record of building things that work. The first lesson is knowing when to cut your losses and pick the pragmatic path.</p>
<p>So, hello, world. There is no &ldquo;About&rdquo; page here. There is only this &ldquo;Posts&rdquo; page.</p>
<p>Because I know for a fact: <strong>this part works.</strong></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
