[{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/blog/","section":"Blog","summary":"","title":"Blog","type":"blog"},{"content":"You can check out the source code here.\nWhat It Does # Visit leorium.link/os0, and it redirects to my first—or rather, zeroth—DuoOS post.\nThen I thought: why stop there? I added QR generation too, so leorium.link/qr/os0 returns a scannable QR code for that same short link.\nWhy I Built It # Honestly, that is already most of the story.\nStill, more seriously, I wanted short, memorable links under my own domain, plus QR codes I could generate on demand for blog posts, videos, projects, and whatever else I end up making. I could have used an existing service, but building my own felt more fun—and, honestly, more fitting.\nSo I built leorium.link: a tiny URL shortener and QR code generator powered by a Cloudflare Worker. Redirect rules live in a simple _redirects file, and every short link can also get a QR code. Small project, very practical, and exactly the kind of personal infrastructure I like having.\nFor example, if I want a short link for a slide deck, I can add one line to the repo and push a commit. About a minute later, it goes live globally. For such a tiny project, the convenience feels almost ridiculous—at least when Cloudflare is behaving.\nI hate to admit it, but I basically vibe-crafted the whole thing. It works, which naturally led to the next question: how does it actually work?\nHow It Works # Overview # The whole thing runs on a Cloudflare Worker. Redirect rules live in a _redirects file in GitHub, and the Worker fetches that file with a short in-memory cache. That means adding a new short link is basically just editing one file and pushing a commit.\nEach redirect can also generate a QR code automatically, which makes it surprisingly useful for blog posts, project pages, and anything else I might want to share quickly.\nThe Deep Dive # At the HTTP level, this whole project is really about one thing: returning the right response.\nSometimes that response is a redirect (301 or 302). Sometimes it is an image. And sometimes, when nothing matches, it falls back to my main site.\nA rough mental model of the request flow looks like this:\nThe browser does a DNS lookup for leorium.link, resolving the domain to Cloudflare\u0026rsquo;s network. The browser sends an HTTP request such as GET /os0 HTTP/1.1. Cloudflare receives the request and runs the Worker instead of serving a static file directly. The Worker checks the path and decides what to return. It responds with either: a redirect response, or a PNG image in the QR-code case. The browser receives that response and handles it accordingly. For a normal short link such as leorium.link/os0, the Worker returns a redirect response with a Location header pointing to the destination URL. The browser then follows that redirect and loads the final page.\nA Lesson About 301 vs. 302 # This tiny project also taught me a surprisingly useful lesson about HTTP redirects.\nAt one point, I added leorium.link/yt to point to my YouTube channel, but it refused to work correctly in my browser. Oddly enough, it worked fine on my phone.\nAfter a bit of reconfiguring—and some back-and-forth with Claude—I realized the problem was probably the browser itself.\nA 301 Moved Permanently response tells clients that the redirect is meant to be permanent, so browsers often cache it aggressively. A 302 Found, on the other hand, is treated as temporary, which makes it much safer while redirect rules are still changing.\nIn my case, the browser had cached an earlier 301 redirect, so even after I changed the Worker logic, it kept sending me to the old destination.\nThe real fix was to use 301 only for redirects I intended to be permanent, and 302 for the fallback path. But because the old redirect was already cached in my browser, I still could not verify the new behavior right away.\nThe immediate fix was even simpler: I just cleared the cached website data. In Safari, that is Option + Command + E if the Develop menu is enabled. Right after that, everything worked as expected.\nSmall project, but surprisingly educational.\nNow, let\u0026rsquo;s look at the Worker itself.\nWalking Through the Worker # You can check out the Worker script here on GitHub.\nShow worker.js source code const REDIRECTS_URL = \u0026#34;https://raw.githubusercontent.com/LeoriumDev/leorium.link/main/_redirects\u0026#34;; let cache = null; let cacheTime = 0; async function getRedirects() { if (cache \u0026amp;\u0026amp; Date.now() - cacheTime \u0026lt; 60000) return cache; // 1 min cache const res = await fetch(REDIRECTS_URL); const text = await res.text(); const map = {}; for (const line of text.split(\u0026#34;\\n\u0026#34;)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith(\u0026#34;#\u0026#34;)) continue; const [slug, url] = trimmed.split(/\\s+/); if (slug \u0026amp;\u0026amp; url \u0026amp;\u0026amp; slug !== \u0026#34;/\u0026#34;) map[slug.replace(\u0026#34;/\u0026#34;, \u0026#34;\u0026#34;)] = url; } cache = map; cacheTime = Date.now(); return map; } export default { async fetch(request) { const path = new URL(request.url).pathname.slice(1).replace(/\\/$/, \u0026#34;\u0026#34;); if (path.startsWith(\u0026#34;qr/\u0026#34;)) { const slug = path.replace(\u0026#34;qr/\u0026#34;, \u0026#34;\u0026#34;); const target = `https://leorium.link/${slug}`; const qr = await fetch(`https://api.qrserver.com/v1/create-qr-code/?size=512x512\u0026amp;data=${encodeURIComponent(target)}`); return new Response(qr.body, { headers: { \u0026#34;Content-Type\u0026#34;: \u0026#34;image/png\u0026#34;, \u0026#34;Cache-Control\u0026#34;: \u0026#34;public, max-age=86400\u0026#34; } }); } const redirects = await getRedirects(); if (redirects[path]) return Response.redirect(redirects[path], 301); return Response.redirect(\u0026#34;https://leorium.com\u0026#34;, 302); } }; At its core, the Worker is just a small JavaScript request handler. It exposes a fetch() method, which receives the incoming HTTP request from the browser and decides what response to return.\nIn practice, that starts by parsing the request URL, extracting the path, and trimming a trailing slash so that /os0 and /os0/ are treated the same—a small detail I learned the hard way while debugging past midnight.\nNext, the Worker checks whether the path starts with qr/. If it does, it strips that prefix, reconstructs the corresponding short URL, and sends that URL to a QR code API.\nImportantly, the QR code is generated for the short link itself, not the final destination. That way, I can change the redirect target later without needing to regenerate the QR code.\nIf the path is not a QR route, the Worker calls getRedirects(). That function keeps a simple in-memory cache for one minute so the Worker does not fetch _redirects from GitHub on every request.\nI picked one minute as a tradeoff: long enough to avoid hitting GitHub unnecessarily, but short enough that updates still feel almost instant.\nOnce the cache expires, the Worker fetches _redirects again, parses it into a map, and checks whether the requested slug exists. If it does, the Worker returns a 301 redirect. If not, it falls back to https://leorium.com because I have not written a proper 404 Not Found page for leorium.link yet.\nYou might ask: why not hardcode the redirects directly inside the Worker?\nThat would certainly be simpler at first. In fact, that was one of Claude\u0026rsquo;s early suggestions. But I wanted the redirect rules to be easy to manage without touching the Worker code itself.\nKeeping them in a separate _redirects file means I can update links with a tiny commit instead of editing and redeploying the Worker every single time.\nIn other words, I separated the routing data from the routing logic. That is partly engineering discipline, and partly laziness.\nAnd that is basically the whole system.\nWhat\u0026rsquo;s Next # The next step is to make it less manual.\nI plan to add a shortener: \u0026lt;redirect-link\u0026gt; field to my blog posts\u0026rsquo; front matter. Then a GitHub Action could scan all posts, extract the shortener fields, generate a redirects file, and sync it to the leorium.link repo.\nAt that point, the Worker could combine auto-generated redirects from Hugo front matter with manual redirects for things like YouTube, social links, and other non-blog pages.\nI also want to build a small homepage for leorium.link where I can browse my short links, and eventually add a proper fallback page for unmatched slugs instead of redirecting everything to my main site.\nBut that is a project for another day.\nDisclosure # AI was used to help refine the writing. All ideas, experiences, and original content are my own.\n","date":"April 7, 2026","externalUrl":null,"permalink":"/blog/leorium-link/","section":"Blog","summary":"","title":"Building leorium.link: A Tiny URL Shortener and QR Generator","type":"blog"},{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/tags/cloudflare-workers/","section":"Tags","summary":"","title":"Cloudflare Workers","type":"tags"},{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/categories/learning/","section":"Categories","summary":"","title":"Learning","type":"categories"},{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/","section":"Leorium","summary":"","title":"Leorium","type":"page"},{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/tags/leorium.link/","section":"Tags","summary":"","title":"Leorium.link","type":"tags"},{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/tags/qr-code/","section":"Tags","summary":"","title":"QR Code","type":"tags"},{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"April 7, 2026","externalUrl":null,"permalink":"/tags/url-shortener/","section":"Tags","summary":"","title":"URL Shortener","type":"tags"},{"content":"","date":"April 6, 2026","externalUrl":null,"permalink":"/tags/bare-metal/","section":"Tags","summary":"","title":"Bare-Metal","type":"tags"},{"content":"","date":"April 6, 2026","externalUrl":null,"permalink":"/series/duoos/","section":"Series","summary":"","title":"DuoOS","type":"series"},{"content":"","date":"April 6, 2026","externalUrl":null,"permalink":"/tags/duoos/","section":"Tags","summary":"","title":"DuoOS","type":"tags"},{"content":"","date":"April 6, 2026","externalUrl":null,"permalink":"/blog/duo-os/","section":"Blog","summary":"","title":"DuoOS Series","type":"blog"},{"content":"","date":"April 6, 2026","externalUrl":null,"permalink":"/tags/milk-v-duo/","section":"Tags","summary":"","title":"Milk-v Duo","type":"tags"},{"content":"","date":"April 6, 2026","externalUrl":null,"permalink":"/tags/os/","section":"Tags","summary":"","title":"OS","type":"tags"},{"content":"","date":"April 6, 2026","externalUrl":null,"permalink":"/tags/risc-v/","section":"Tags","summary":"","title":"RISC-V","type":"tags"},{"content":"","date":"April 6, 2026","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":" The Spark # Fortune favors the bold.\n— Virgil\nThe story of DuoOS begins at MOPCON 2025. My junior, Brian Duan, led the scheduling team, and I was part of it. He knew Alan Jian from NYCU, who had previously studied at Wu-Ling Senior High School (WLSH) and was already known as an absolute beast in low-level systems. I heard they were planning to build an operating system from scratch, so I wanted Brian to introduce me. But he was too shy and thought I was being kind of weird about it, so I messaged Alan directly on Discord, introduced myself, and somehow got invited into Libresys (formerly NexOSS).\nRight after MOPCON 2025, the semester was coming to an end. During winter break, Alan invited us to a meetup in Taipei called Libresys Party, and that was when he gave me this Milk-V Duo board. The board definitely collected some dust for a while, but much later, I suddenly had a thought: what if I tried building my own operating system on this little thing?\nAt the time, I had originally been planning to build my own compiler, and I was working on a C parser/generator library. But from that moment on, things took a very different turn. That was the beginning of a strange and wonderful journey—and, well, the rest is history.\nThe Milk-V Duo board that started it all Why Reinvent the Wheel? # As of 2026, AI has become deeply embedded in everyday life. I say this not as an outsider, but as someone who actively uses it myself—I use Claude Code Max too. And yet, deep down, I know there are times when I\u0026rsquo;m not really thinking, but merely offloading cognition to AI. In that sense, it feels less like artificial intelligence and more like borrowed intelligence.\nAs jserv once put it:\n手機出來了，大家都不太去運動了；人工智慧出來了，大家都不太動腦子了。\nWhich roughly translates to: \u0026ldquo;When smartphones came along, people stopped moving as much. When AI came along, people stopped thinking as much.\u0026rdquo;\nThanks to the rise of AI, the gap between a random person and someone who has spent four years grinding through computer science has, in some ways, never felt narrower. Today, anyone can open ChatGPT and type, \u0026ldquo;Build me an accounting app.\u0026rdquo; The software industry was already fast-paced to begin with, but AI has accelerated it to an entirely new level.\nThat raises an uncomfortable question: what exactly is the difference between a random person with AI access and someone who has spent years seriously studying computer science?\nI have to admit: that question hits hard. Before I started this project, my honest answer was, \u0026ldquo;Not much.\u0026rdquo;\nAnd that realization bothered me more than I wanted to admit. How could I justify being better at software engineering than a random AI user—or what people call a \u0026ldquo;vibe coder\u0026rdquo;—if I didn\u0026rsquo;t truly understand the foundations myself? How could I stand out if all I could do was produce results with the help of tools that everyone else also had access to?\nAs Richard Feynman famously said:\nWhat I cannot create, I do not understand.\n— Richard Feynman\nTo truly understand something, I need to be able to build it myself.\nBut why build an operating system from scratch specifically? Because of its granularity. An operating system forces you to care about everything. It pushes you closer to the machine than most software ever will. A single mistake can crash the system, reboot the machine, or corrupt data. There is very little room to fake understanding.\nAnd that is exactly why I chose it. Only by trying to build an OS have I started to appreciate just how much modern operating systems abstract away—the messy, fragile, hardware-facing details that most of us rarely have to confront directly. Building one means stripping away that comfort and facing the raw machine more honestly.\nThat, ultimately, is why I started this project. I don\u0026rsquo;t want to depend on borrowed intelligence forever. I want to develop the kind of understanding that makes me genuinely different—the kind that is harder to imitate, harder to replace, and earned through real thought.\nHopefully, through this kind of training, I can become someone set apart by real understanding.\nWhat DuoOS Actually Is # DuoOS is a minimal RISC-V operating system for the Milk-V Duo, built primarily as a learning project.\nI want to build it the hard way, with as little abstraction as possible—but not recklessly hard. For now, I am accepting a few \u0026ldquo;training wheels,\u0026rdquo; namely OpenSBI and U-Boot, so that my first pass at OS development does not collapse under too much upfront complexity. I want the challenge to be real, but still bearable.\nAt its core, DuoOS is a C operating system for RISC-V, built to answer a simple question: how does an operating system actually work under the hood? It may remain minimal. It may never become complete. But that is not really the point. The point is to learn by building.\nAnd that brings me to how this project actually unfolded—through three separate attempts.\nHow It Actually Went # Action without thought is empty. Thought without action is blind.\n— Kwame Nkrumah\nI am a world-class procrastinator. The dangerous thing about procrastination is that it is really good at disguising itself as productivity. You tell yourself you\u0026rsquo;re just looking for the best resources, the best roadmap, or the best way to begin. Before you know it, you\u0026rsquo;re deep in analysis paralysis, genuinely believing you\u0026rsquo;re putting in the work, when in reality it\u0026rsquo;s just low-effort busywork. But this time, I told myself, it was time to change that.\nThe First Attempt # My first attempt began on March 25, 2026. It was a sunny afternoon. I looked at my Milk-V Duo sitting on the shelf and thought, \u0026ldquo;Maybe I can do something more than just let it sit there collecting dust.\u0026rdquo;\nThen one idea hit me: \u0026ldquo;Let\u0026rsquo;s build an OS on it.\u0026rdquo;\nBut how? Like any ordinary person living in 2026, I turned to AI for help—namely Claude. After a fair amount of tweaking and fighting with it, I managed to build the very first version of DuoOS. But right from the beginning, I knew deep down that I did not truly understand what the hell I had built. That realization led to the second attempt.\nFirst Attempt on DuoOS The Second Attempt # AI has both pros and cons. Used wisely, it can greatly improve both learning and productivity. This time, I tried to use it more deliberately.\nThe next day, March 26, 2026, I used Claude for deeper research, and that eventually led to a major revamp of DuoOS. It generated a long research guide on how to build an operating system from scratch. Through that process, I started to understand much more of the actual path from bare metal to a working system: linker scripts, ELF loading, device trees, image layout, how U-Boot loads a program, and many other foundational details. I learned a lot from that stage.\nAfter the initial setup, I could at least say that I had managed to run a bare-metal program on the Milk-V Duo. That alone already felt like a small win. But that was still far from an actual operating system. So I moved on to writing a basic shell and a printf-like function—yes, you read that correctly, because printf does not simply exist for you in a bare-metal environment.\nAfter that, I started implementing more features: boot-time information, LED control, timer support, hart-related commands, watchdog timer handling, trap handling, timer interrupts, and an uptime command.\nBut during debugging, another thought hit me: why not document all of this? At the same time, I also realized the codebase already contained too many bugs, too many bad decisions, and too much messy code. So I decided to start over again.\nAs the saying goes, third time\u0026rsquo;s the charm.\nSecond Attempt on DuoOS The Third Attempt # I have not started the third attempt yet, but I already know this time will be different. I asked Claude to generate a more thorough and better-structured guide for building the system step by step, all the way toward networking.\nThe scope is honestly a little wild: UART I/O, trap handling, virtual memory, process scheduling, user mode, fork/exec, a FAT32 filesystem, an ELF loader, Unix pipes, and eventually a working shell—all the way from bare metal upward.\nThat is the version I plan to document in the next series of blog posts.\nNote: The reason this is only being written on April 6 is partly my own world-class procrastination, and partly the usual interruptions from university quizzes and coursework.\nConclusion # Be the designer of your world and not merely the consumer of it.\n— James Clear\nFrom here on, I\u0026rsquo;ll be documenting the entire journey in this series. If you want to follow along, check out the DuoOS series.\nDisclosure # AI was used to help refine the writing. All ideas, experiences, and original content are my own.\n","date":"April 6, 2026","externalUrl":null,"permalink":"/blog/duo-os/introduction/","section":"Blog","summary":"","title":"Why I'm Building My Own Operating System","type":"blog"},{"content":"This site is built with Hugo using the Blowfish theme, hosted on Cloudflare Pages, and the source is on GitHub.\n","externalUrl":null,"permalink":"/colophon/","section":"Leorium","summary":"","title":"Colophon","type":"page"},{"content":"DuoOS on GitHub\nA minimal bare-metal RISC-V operating system built from scratch, targeting the Milk-V Duo.\nOverview # DuoOS is a personal systems project focused on learning operating systems from first principles by building one directly on bare metal. The goal is to understand the full path from bootstrapping and low-level hardware interaction to core kernel mechanisms such as memory management, traps, scheduling, and device support.\nGoals # Build a small but real bare-metal OS from scratch Learn RISC-V systems programming deeply Understand kernel internals through implementation Document the process clearly through a long-form blog series Target Platform # Architecture: 64-bit RISC-V Board: Milk-V Duo Current Status # DuoOS is being rewritten from scratch (third attempt) with full documentation.\nImplemented (previous iterations) # Bootstrapping (entry.S, linker script, FIT image, U-Boot boot) UART I/O via SBI ecalls printf with variadic args (%d, %x, %s, %c, %p) Interactive shell with tokenizer and command dispatch LED control via GPIO MMIO Boot timer (rdtime, 25 MHz) Watchdog timer reboot Trap handler (scause/sepc/stval diagnostics) Timer interrupts Planned # Virtual memory (Sv39 page tables) Process scheduling User mode + syscalls fork/exec/wait/exit FAT32 filesystem ELF loader Unix pipes Writing # Related posts and development notes are collected in the DuoOS series.\nWhy this project exists # I built DuoOS to learn operating systems the hard way: by writing one. Instead of treating OS concepts as purely theoretical material, this project turns them into something concrete, inspectable, and incremental.\nLinks # Github Repository: LeoriumDev/duo-os Blog Series: /blog/duo-os/ ","externalUrl":null,"permalink":"/projects/duo-os/","section":"Projects","summary":"A minimal bare-metal RISC-V OS built from scratch for the Milk-V Duo.","title":"DuoOS","type":"projects"},{"content":"Updated April 2026.\nBuilding DuoOS, a minimal bare-metal RISC-V OS built from scratch for the Milk-V Duo. ","externalUrl":null,"permalink":"/now/","section":"Leorium","summary":"","title":"Now","type":"page"},{"content":"A selection of projects I’ve built and am documenting over time.\n","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"","title":"Projects","type":"projects"},{"content":" BestAIAwards # slides: Click Here Team leader: Meng-Shao Liu (劉孟劭) Contact: sau525@gmail.com Members: Xue-Li He (何學禮) Contact: contact@leorium.com Yung-Tai Chuang (莊永太) Contact: chuangyungtai@gmail.com Yu-Chuan Chang (張祐銓) Contact: chang.oscarx@gmail.com ","externalUrl":null,"permalink":"/nanobrain/","section":"Leorium","summary":"","title":"Team Nanobrain","type":"page"}]