<?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/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Fieldnotes by Ellie Augustini</title><link>https://augustini.wtf/fieldnotes/</link><description>Technical fieldnotes about infrastructure, self-hosting, firmware, terminal interfaces, and engineering incidents that escalated into essays.</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Tue, 14 Jul 2026 14:00:00 +0200</lastBuildDate><atom:link href="https://augustini.wtf/fieldnotes/index.xml" rel="self" type="application/rss+xml"/><item><title>At some point, every application became a landlord</title><link>https://augustini.wtf/fieldnotes/why-i-built-a-bare-metal-cluster/</link><pubDate>Tue, 14 Jul 2026 14:00:00 +0200</pubDate><guid isPermaLink="true">https://augustini.wtf/fieldnotes/why-i-built-a-bare-metal-cluster/</guid><dc:creator>Ellie Augustini</dc:creator><category>bare-metal</category><category>self-hosting</category><category>privacy</category><category>saas</category><category>ai</category><category>encryption</category><category>digital-sovereignty</category><category>kubernetes</category><description>&amp;lt;p&amp;gt;At some point, every application became a landlord.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Your photos live in one company’s building. Your email lives in another. Your documents, passwords, conversations, calendar, finances, recipes, artwork and half-finished thoughts each occupy a furnished little room in somebody else’s data centre. The rent looks harmless because it arrives in twelve separate subscriptions, each priced somewhere between “a coffee” and “surely I cancelled this.”&amp;lt;/p&amp;gt;</description><content:encoded>&amp;lt;p&amp;gt;At some point, every application became a landlord.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Your photos live in one company’s building. Your email lives in another. Your documents, passwords, conversations, calendar, finances, recipes, artwork and half-finished thoughts each occupy a furnished little room in somebody else’s data centre. The rent looks harmless because it arrives in twelve separate subscriptions, each priced somewhere between “a coffee” and “surely I cancelled this.”&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Then the landlord changes the terms.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The free tier shrinks. The export button develops strong religious objections to portability. A feature you relied on moves into the Business Pro Max Enterprise Friendship Edition. The monthly price rises because the product now contains an AI assistant you did not request and cannot fully disable. A new paragraph appears in the terms of service granting the company broader rights to process what you upload. You receive an email titled &amp;lt;strong&amp;gt;We’re updating our terms&amp;lt;/strong&amp;gt;, written in the warm, bloodless dialect corporations use when the update is mandatory and the explanation would frighten the horses.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Your options are to accept or leave.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Leaving means discovering that five years of your life have been stored in a format best described as “technically JSON.”&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is why I built a bare-metal cluster.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I built it because convenience had slowly turned into dependency, dependency had turned into exposure, and exposure had started to feel reckless.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I wanted a boundary I controlled.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The decision was less philosophical than the rest of this essay may make it sound. I looked at where the authoritative copies of my life actually lived and realised that almost none of them were under my control. Photographs existed primarily in somebody else’s library. Documents depended on accounts that could be repriced or closed. Conversations and creative work were scattered across companies whose future owners I could not predict and whose terms I could not negotiate. There was no single cinematic betrayal; just more irreplaceable data and fewer credible exit routes.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;So I rented two dedicated machines. Each has a six-core Ryzen 5 3600 and 64 GiB of memory. One contributes two 2 TB spinning disks; the other has two 512 GB NVMe drives. Talos runs the nodes, Kubernetes schedules the applications, Cilium moves the packets, Rook Ceph turns four disks into storage, and OpenTofu records enough of the arrangement that future me has at least a fighting chance.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is not the architecture I would sell to a bank. It is the architecture I chose for one person who wants custody, useful capacity and the ability to replace parts without rebuilding an entire digital life from memory.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;we-asked-for-the-cloud-and-for-good-reasons&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;We asked for the cloud, and for good reasons&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#we-asked-for-the-cloud-and-for-good-reasons&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: We asked for the cloud, and for good reasons&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;SaaS did not conquer the world solely through deceit and decorative gradients. It solved real problems.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Software used to arrive in a box. You bought a version, installed it, discovered that the installer needed three floppy disks in an order known only to the Oracle at Delphi, and eventually owned a working copy. Businesses ran servers in cupboards where the air conditioning was theoretical and the backup tape had last been tested during the Blair administration. If the disk died, your software did not float gracefully into another availability zone. It became a learning opportunity.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Salesforce made the subscription model culturally legible in 1999: business software delivered through a browser, without every customer maintaining the whole stack. Then Amazon launched S3 and EC2 in 2006, turning storage and compute into APIs instead of a procurement process involving six signatures and a Dell sales representative. The cloud made infrastructure previously reserved for large organisations available to anyone with a payment card and insufficient supervision.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That change was genuinely liberating. Small teams could deploy globally. Managed databases removed a category of 03:17 phone calls. PaaS products went further: push code, let somebody else care about the operating system, routing, certificates, autoscaling and the endless parade of CVEs produced by software continuing to exist.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Each layer removed work. Each layer also removed control.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;At first that exchange was obviously worthwhile. Usually it still is. I do not want every hospital writing its own video-conferencing stack or every bakery operating an email server because somebody on the internet shouted “digital sovereignty” through a mouthful of ethernet cable.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The problem was not renting software.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The problem was allowing rented software to become the only place our lives existed.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Cloud software is different. What you buy is not an object. It is an ongoing relationship with an organisation whose incentives can change.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The service can be excellent and the people building it can care deeply. None of that suspends economics. The company may be acquired; investors may demand growth; the cheap product that attracted millions may now be expected to extract more money from them. Previously included features migrate upward through pricing tiers like frightened woodland animals escaping a forest fire.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;And the company has three assets it can monetise:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ol&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;The product.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Your attention.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Your data.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ol&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The first is respectable but apparently insufficient for the infinite-growth machine. The second gave us the advertising internet. The third has become extremely interesting now that every executive presentation contains the phrase &amp;lt;strong&amp;gt;AI strategy&amp;lt;/strong&amp;gt; and at least one arrow pointing toward a cylinder labelled &amp;lt;code&amp;gt;DATA&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I do not need to believe everyone involved is malicious. Incentives are more scalable than malice.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;ai-made-the-ownership-problem-impossible-to-ignore&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;AI made the ownership problem impossible to ignore&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#ai-made-the-ownership-problem-impossible-to-ignore&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: AI made the ownership problem impossible to ignore&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Generative AI did not invent extractive data practices. Advertising platforms, data brokers and recommendation systems had already industrialised those. AI changed the perceived value of the material.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Photos became potential training data. Posts became a language corpus. Voice recordings became speech data. Documents, illustrations and source code became examples from which models might learn.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The response across the industry has not been uniform, and accuracy matters here. The policies below were checked in July 2026; terms mutate like bacteria under a heat lamp. “Every company trains on all your private data” is a satisfying sentence and a bad argument.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Adobe prompted users to re-accept terms in 2024 and caused an entirely predictable panic among creators. Adobe subsequently clarified that it had not trained generative AI on customer content and promised to put that commitment into its legally binding terms. Its own explanation acknowledged that companies hosting creative work must be precise about &amp;lt;a href=&amp;#34;https://blog.adobe.com/en/publish/2024/06/10/updating-adobes-terms-of-use&amp;#34;&amp;gt;what rights they claim and why&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That was a better outcome than people initially feared. It also demonstrated the power imbalance: each creator had to determine whether a broad licence was operational boilerplate or a combine harvester approaching their life’s work.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Other companies are explicit about using content. Meta announced plans to train its models on public posts, comments, photos and captions shared by adults in Europe, with a process through which users could object. Meta’s own description says exactly that: it wants to train on &amp;lt;a href=&amp;#34;https://about.fb.com/news/2025/04/making-ai-work-harder-for-europeans/&amp;#34;&amp;gt;public content from European users&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Public posts are not private photo libraries, product improvement is not always generative-model training, and an opt-out is not an opt-in. We should criticise what companies actually do, not flatten everything into a single sinister blob wearing a conference lanyard.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;But the pattern still bothers me. The default relationship is increasingly that a service receives broad access, the policy evolves, and the user is responsible for noticing, understanding and finding the correct toggle. Consent becomes a scavenger hunt conducted inside an interface the company can redesign tomorrow.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I am not against AI. I &amp;lt;a href=&amp;#34;/fieldnotes/hacking-mealie-for-fun-and-dinner/&amp;#34;&amp;gt;patched Mealie to use a language model for recipe imports&amp;lt;/a&amp;gt;, an act that combined artificial intelligence, Kubernetes and dinner with a level of restraint rarely seen outside defence procurement.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Machine learning is useful. Generative systems can translate, summarise, classify, help people write code and make interfaces more accessible.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The unresolved question is what we are entitled to feed them.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Training useful models requires a great deal of data. That does not make every accessible work ethically ownerless. “It was on the internet” is a description of network reachability, not a theory of consent. A forum post written to help one stranger debug a kernel panic was not necessarily offered as free industrial feedstock forever.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;My position is not “stop developing models.” Tell people what is collected. Distinguish operating a service from training a new product. Ask before using private material, make refusal as easy as acceptance, respect creators’ reservations, and document sources well enough that accountability is possible. Do not call extraction innovation merely because it ends in matrix multiplication.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I will send some data to an AI service when the purpose is clear and the material appropriate. That is different from storing the canonical copy of my private life there and hoping its next business model remains charming. Agency is the point.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;privacy-is-not-a-confession&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Privacy is not a confession&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#privacy-is-not-a-confession&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Privacy is not a confession&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Whenever privacy comes up, somebody announces that they have nothing to hide.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This sounds brave until you ask them to publish their passwords, medical history, private messages, bank statements, precise location, family photographs, search history and every unfinished thought they typed at 01:40 and wisely deleted.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Nobody actually has nothing to hide. What they mean is that they do not currently expect the information collected about them to be used by somebody hostile.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That is a prediction about power, not a fact about data.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Privacy scholar Daniel Solove has spent years dismantling the “nothing to hide” argument. Its central failure is treating privacy only as secrecy about wrongdoing. Privacy is also protection against aggregation, exclusion, distortion, secondary use, loss of context and systems making decisions about us that we cannot inspect or challenge. His essay, &amp;lt;a href=&amp;#34;https://papers.ssrn.com/sol3/papers.cfm?abstract_id=998565&amp;#34;&amp;gt;“I’ve Got Nothing to Hide” and Other Misunderstandings of Privacy&amp;lt;/a&amp;gt;, remains annoyingly relevant.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;One isolated fact may be harmless. A collection becomes a map.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Your photographs reveal relationships, locations, homes, routines and health. Email reveals who you know and which institutions you depend on. Calendar entries reveal where you will be. Search history reveals fears before you have words for them. Metadata can reveal that two people communicate regularly even when the messages themselves are encrypted.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The danger also changes over time. Data collected under one government, one management team or one social norm remains available when those conditions change.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This matters to everyone. It matters especially to people whose safety has historically depended on controlling context.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I am queer. With the brown wave rising again across Europe and elsewhere, I do not consider it paranoid to ask how identity, relationships, location and private communication could be used under a more hostile political order. Rights that look settled can become campaign material. Healthcare can become evidence. A social graph can become an investigative lead. A list created for advertising can become useful to an authority with a different mandate.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The Electronic Frontier Foundation and Access Now have warned that data concerning sexual orientation and gender identity can enable surveillance and violence against LGBTQ+ people, and have argued for stronger protection of that data in a &amp;lt;a href=&amp;#34;https://www.eff.org/files/2024/01/31/access_now_eff_written_submission_un_ie_on_sogie_-_jan_2024_0.pdf&amp;#34;&amp;gt;submission to the UN Independent Expert on sexual orientation and gender identity&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;You do not build privacy only for the government you trust.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;You build it for the next government, the next acquisition, the next database breach, the next policy update and the next executive who looks at a table of user content and sees an unmonetised asset.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;encryption-changes-who-is-capable-of-betraying-you&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Encryption changes who is capable of betraying you&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#encryption-changes-who-is-capable-of-betraying-you&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Encryption changes who is capable of betraying you&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;A privacy policy is a promise. End-to-end encryption is an architectural constraint.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Those are not equivalent.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;If a service can read your data, it may do so for legitimate operational reasons. It can also be breached, compelled, acquired, misconfigured or persuaded by a future policy. Access may be carefully audited and restricted, but the capability exists.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;With correctly implemented end-to-end encryption, the service does not possess the keys required to read the content. It can still expose metadata, clients can still be compromised, participants can still take screenshots, and backups can quietly ruin the whole arrangement if designed by a committee. E2EE does not solve every problem. It does remove the service operator from one extremely important trust boundary.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Self-hosting and end-to-end encryption solve different problems. Self-hosting changes who operates the service. E2EE limits what even the operator can read. Where possible, I want both.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Signal describes the principle plainly: its service is designed so it does not have access to message contents, and it also works to minimise retained metadata. The expensive part of Signal is not drawing a blue send button; it is maintaining a system deliberately built to know less about its users. Its explanation of the costs and architecture is worth reading: &amp;lt;a href=&amp;#34;https://signal.org/blog/signal-is-expensive/&amp;#34;&amp;gt;privacy is priceless, but Signal is expensive&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;European data-protection authorities likewise describe encryption as necessary for privacy and free expression. The European Data Protection Supervisor warns that weakening or circumventing E2EE through backdoors or key escrow destroys its effective protection. Its &amp;lt;a href=&amp;#34;https://www.edps.europa.eu/data-protection/our-work/subjects/encryption_en&amp;#34;&amp;gt;encryption overview&amp;lt;/a&amp;gt; is much less ambiguous than most political proposals involving the phrase “lawful access.”&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The recurring demand for systems that are private except when the correct authority asks is cryptographic astrology. A backdoor available only to good people is not a security property. It is a staffing assumption.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;For queer people, journalists, activists, abuse survivors, migrants, healthcare workers and anyone living under a government with authoritarian ambitions, secure communication is not a decorative civil liberty. It is infrastructure.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;why-bare-metal-and-why-a-cluster&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Why bare metal, and why a cluster?&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#why-bare-metal-and-why-a-cluster&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Why bare metal, and why a cluster?&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The argument so far would justify a NAS, a VPS running Docker Compose, a managed Nextcloud provider, or three Raspberry Pis committing distributed-computing offences behind the television.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;All of those can be good answers. Mine was dedicated servers because the workload stopped being one application and started becoming a small platform.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Photos are the largest part. Immich wants substantial storage, background processing, PostgreSQL with vector search, a machine-learning worker and room for the library to grow. Matrix, Vaultwarden, Mealie, Actual Budget, Outline, Grist, SearXNG, monitoring, CI and the rest add databases, caches, object storage and persistent volumes. Individually they are modest. Together they want predictable memory, direct disks and enough spare capacity that an update does not turn the scheduler into a Victorian workhouse.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Bare metal gives me dedicated RAM and CPU, direct access to the storage devices, predictable monthly capacity and no metered egress between my own workloads. Ceph can use the real disks rather than virtual volumes rented from the same abstraction it is trying to replace. Large photo and backup transfers do not produce a bill that reads like a ransom note.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Why two machines? One server would be simpler, but compute, storage and the control plane would all share one machine and one failure domain. Because both nodes are schedulable, Kubernetes can move stateless workloads and give me somewhere to move work while maintaining a node. Some databases can also place replicas apart. The second machine added fast NVMe storage alongside the larger spinning disks.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Why Kubernetes? Because it is the operational model I know and enjoy. It gives me declarative deployments, health checks, scheduling, identity integration, storage primitives and enough YAML to prevent dangerous levels of free time. Most of these applications could run under Docker Compose. I am not claiming a Deployment object makes the grocery list taste better.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Why Ceph? Applications need several kinds of persistence. RBD provides block volumes, CephFS provides shared filesystems, and RGW provides S3-compatible object storage. One storage system can serve PostgreSQL volumes, application PVCs, media and backup objects while allowing a disk to be replaced without teaching every application a new ritual.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Now for the sentence storage people have been holding their breath for: two nodes do not make a production-grade Ceph cluster.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There are four OSDs across the two machines and pools use two replicas with the CRUSH failure domain set to &amp;lt;code&amp;gt;osd&amp;lt;/code&amp;gt;, not &amp;lt;code&amp;gt;host&amp;lt;/code&amp;gt;. Ceph guarantees different OSDs, but both copies may still land on the same machine. The layout protects against an individual OSD failure; losing a host can mean restoration, not merely an outage. The cluster also has one Kubernetes control-plane node and one Ceph monitor.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This design is distributed. It is not highly available.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That is a deliberate cost and complexity boundary, not a hidden triumph. A third node would allow proper etcd and Ceph monitor quorum and stronger host-level replica placement. Until then, the second node improves maintenance options and some failure handling; it does not transform two computers into three.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The answer to “what failure can this survive?” is therefore specific: individual pods, processes and OSDs can fail without losing the whole service. A node or control-plane failure becomes a recovery event rather than a transparent shrug. Loss of the entire cluster requires restoration from somewhere that is not the cluster.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Distributed systems remain stubbornly opposed to inspirational arithmetic.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-cheapest-invoice-is-not-always-the-lowest-cost&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The cheapest invoice is not always the lowest cost&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-cheapest-invoice-is-not-always-the-lowest-cost&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The cheapest invoice is not always the lowest cost&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is where self-hosting evangelists traditionally produce a spreadsheet proving they save €14 per month, provided their labour has no value and hard drives emerge spontaneously from woodland soil.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Together, including their IP addresses, the two servers cost €78.80 per month before VAT, or €98.50 with Swedish VAT. They replace several subscriptions and provide terabytes of raw storage, 128 GiB of memory and twelve physical CPU cores across the cluster. Buying that capacity as separate managed databases, object storage, photo hosting, CI, monitoring and application plans would be expensive. Buying only the few consumer subscriptions I strictly need might be cheaper.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Then there is labour:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Systems administration: financially catastrophic if invoiced honestly&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;I do enjoy this work. Debugging Cilium at two in the morning is not cheaper if accounted for honestly, but it is at least my preferred genre of regrettable decision.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The financial case works for my collection of storage-heavy services. The larger case includes control, portability and privacy. Sometimes the cheapest service has a low invoice because you are paying with lock-in, attention, exposure or the future difficulty of leaving.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;a-boundary-not-a-fortress&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;A boundary, not a fortress&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#a-boundary-not-a-fortress&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: A boundary, not a fortress&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Moving data onto my own infrastructure does not automatically make it safe.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Google has security teams, redundant data centres, hardware supply-chain programmes, abuse detection and people who understand email delivery at a depth normally associated with ocean trenches. I have strong opinions, two servers and a TODO file containing the phrase &amp;lt;strong&amp;gt;do these first&amp;lt;/strong&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The present architecture makes those limits concrete. Database backups written into the same Ceph cluster are useful for logical recovery, but they are not disaster recovery from loss of that cluster. Until encrypted off-site copies, external etcd snapshots and a tested restore path exist, this remains a migration in progress rather than a finished risk-management system. A green dashboard does not negotiate with a failed disk.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Backups are a process. Merely possessing backup-shaped objects is storage-themed optimism.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There is also concentration risk. Earlier I said that a collection becomes a map; this cluster deliberately assembles much of that map inside one administrative domain. A compromised administrator, identity provider or recovery account could reach several services at once.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Some mitigations already exist: applications have separate credentials and namespaces, while sensitive interfaces use restricted network paths. Independent recovery credentials, off-site copies and regular restore drills are still work to complete. One login button across everything is convenient. It is also a blast radius wearing a friendly logo.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Self-hosting changes the threat model. It reduces exposure to platform policy changes and mass commercial data use while increasing exposure to my mistakes, targeted attacks and hardware failures. A personal server is a small target with an administrator who occasionally deploys after midnight.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The correct questions are not “cloud or self-hosted: which is secure?” They are: which failures am I preparing for, who can access the data, who can change the rules, how do I leave, where are the backups, and what happens when I am wrong?&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;If those questions have no answers, the deployment model is branding.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The cloud gave us extraordinary capabilities. I do not want to reverse that history; I want its next part to be less extractive. Convenience should not require surrendering control over our memories and creative work. AI development should not pretend consent is implicit wherever a crawler can reach. Privacy should not be a luxury feature, and encryption should not be weakened because inaccessible data is administratively inconvenient.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Digital sovereignty is not a binary state. It is the practice of reducing unnecessary dependence and making the remaining dependencies explicit.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;You do not need a bare-metal Kubernetes cluster to move in that direction. Export your photos. Keep local copies of documents. Use a password manager and E2EE. Choose services with credible business models and usable exports. Use a NAS if that solves the problem. Learn where your data goes.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Sovereignty is a direction, not a rack size.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;My cluster is not a fortress. It is a boundary. I have not eliminated landlords; I have made them replaceable. A provider may still evict the machines, but it does not get to evict me from my own data. The SaaS vendors I chose not to use never receive the authoritative copy in the first place.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That matters more to me now than it did ten years ago. The industry is consolidating, generative AI has made data newly valuable, and the political weather is getting worse. Under those conditions, keeping intimate information inside systems I can inspect, move and shut down feels less like a hobby and more like basic risk management.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I still love technology. I love it enough to want better from it.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;So I built the cluster: two servers, too many databases, several thousand lines of OpenTofu, and one very expensive way to say that my life is not an untapped dataset.&amp;lt;/p&amp;gt;&amp;#xA;</content:encoded></item><item><title>Hacking Mealie for fun and dinner</title><link>https://augustini.wtf/fieldnotes/hacking-mealie-for-fun-and-dinner/</link><pubDate>Tue, 14 Jul 2026 10:00:00 +0200</pubDate><guid isPermaLink="true">https://augustini.wtf/fieldnotes/hacking-mealie-for-fun-and-dinner/</guid><dc:creator>Ellie Augustini</dc:creator><category>mealie</category><category>openai</category><category>gpt-5.6</category><category>python</category><category>kubernetes</category><category>opentofu</category><category>self-hosting</category><description>&amp;lt;p&amp;gt;&amp;lt;img src=&amp;#34;https://augustini.wtf/fieldnotes/hacking-mealie-for-fun-and-dinner/mealie-recipes_hu_4fa5d2ce769adad4.webp&amp;#34; alt=&amp;#34;My Mealie recipe library, now considerably more international and significantly less allergic to metric units.&amp;#34; width=&amp;#34;1024&amp;#34; height=&amp;#34;566&amp;#34; loading=&amp;#34;lazy&amp;#34; decoding=&amp;#34;async&amp;#34;&amp;gt;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There is a very specific kind of optimism involved in clicking &amp;lt;strong&amp;gt;Import recipe&amp;lt;/strong&amp;gt; and expecting a food blog to become structured data.&amp;lt;/p&amp;gt;</description><content:encoded>&amp;lt;p&amp;gt;&amp;lt;img src=&amp;#34;https://augustini.wtf/fieldnotes/hacking-mealie-for-fun-and-dinner/mealie-recipes_hu_4fa5d2ce769adad4.webp&amp;#34; alt=&amp;#34;My Mealie recipe library, now considerably more international and significantly less allergic to metric units.&amp;#34; width=&amp;#34;1024&amp;#34; height=&amp;#34;566&amp;#34; loading=&amp;#34;lazy&amp;#34; decoding=&amp;#34;async&amp;#34;&amp;gt;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There is a very specific kind of optimism involved in clicking &amp;lt;strong&amp;gt;Import recipe&amp;lt;/strong&amp;gt; and expecting a food blog to become structured data.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The page has a 900-word childhood memory, three newsletter pop-ups, an autoplaying video, twelve affiliate links, two incompatible versions of the recipe, and—somewhere beneath the GDPR consent manager—a list containing &amp;lt;code&amp;gt;1 cup of flour&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Computers love this. It is basically archaeology, except every pottery fragment has a Pinterest button attached.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I only wanted somewhere nice to keep recipes.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;More specifically, I wanted to cook food from more cultures and stop cycling through the same handful of meals like a cron job with low self-esteem. Swedish comfort food is lovely. So are Sichuan pork, Malaysian mushroom korma, Serbian meat rolls, Polish cabbage rolls, Italian ragù, British cakes, and whatever delicious thing I discover at 23:40 while browsing recipes instead of sleeping.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That is how I arrived at &amp;lt;a href=&amp;#34;https://mealie.io/&amp;#34;&amp;gt;Mealie&amp;lt;/a&amp;gt;: an open-source, self-hosted recipe manager. It stores recipes, images, categories and tags; builds meal plans and shopping lists; scales quantities; organizes cookbooks; and imports recipes from the web. In other words, it is the sensible domestic application I needed, so naturally I deployed it onto Kubernetes with PostgreSQL, OIDC, persistent Ceph storage, Gateway API routing, and backups.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Some people keep recipes in a binder. I keep mine behind a CloudNativePG cluster. We all have coping mechanisms.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-incident-report&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The incident report&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-incident-report&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The incident report&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Mealie&amp;amp;rsquo;s URL importer is genuinely useful. Paste a recipe URL and, in the happy path, a complete recipe appears with its title, image, ingredients, instructions, times and servings. That is the kind of feature that feels like magic until you import recipes from several countries and discover that the magic has regional settings.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;One recipe says &amp;lt;code&amp;gt;2 cups flour&amp;lt;/code&amp;gt;. Another says &amp;lt;code&amp;gt;200 millilitres whipping cream&amp;lt;/code&amp;gt;. A third says &amp;lt;code&amp;gt;1 EL Öl&amp;lt;/code&amp;gt;. Temperatures alternate between Celsius and Fahrenheit depending on which side of the Atlantic currently has custody of the oven. Instructions arrive with decorative entries such as &amp;lt;code&amp;gt;Make the sauce&amp;lt;/code&amp;gt; occupying a whole step, followed by the actual instruction in the next step. Ingredient parsing occasionally decides that preparation notes are foods, units are philosophical suggestions, and fractions are an attack on its family.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;What I wanted was less “preserve whatever the page happened to publish” and more this:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Source:&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;1 cup water&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Bake at 375°F.&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Make the sauce:&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Add the garlic, lemon juice and stock to the pan.&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Normalized:&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;240 ml water&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Bake at 190°C.&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;To make the sauce, add the garlic, lemon juice and stock to the pan.&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Nothing revolutionary. Just consistent measurements, an oven setting compatible with the continent where my oven lives, and instructions that do not mistake typography for cooking.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Translation adds another layer. I do not want culturally specific dish names flattened into bland English product copy, but I do want instructions in one language I can reliably follow while holding a hot pan. &amp;lt;code&amp;gt;Hui Guo Rou&amp;lt;/code&amp;gt; should remain &amp;lt;code&amp;gt;Hui Guo Rou&amp;lt;/code&amp;gt;; the paragraph explaining when to add the doubanjiang can be English. This is a semantic decision, not a string replacement.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Under the hood, Mealie&amp;amp;rsquo;s first URL strategy uses &amp;lt;a href=&amp;#34;https://github.com/hhursev/recipe-scrapers&amp;#34;&amp;gt;hhursev/recipe-scrapers&amp;lt;/a&amp;gt;, an impressive Python library with parsers for hundreds of recipe sites plus schema.org/JSON-LD support. For extraction, this is excellent engineering. Most competent recipe sites already publish machine-readable &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; data because search engines reward them for doing so. The scraper can collect that data deterministically, quickly, locally, and without paying an API bill every time somebody wants soup.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The limitation is not that &amp;lt;code&amp;gt;recipe-scrapers&amp;lt;/code&amp;gt; fails at its job. The limitation is that its job is &amp;lt;strong&amp;gt;scraping&amp;lt;/strong&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;It can tell me the source says &amp;lt;code&amp;gt;1 cup all-purpose flour&amp;lt;/code&amp;gt;. It should not casually decide whether that means 120 g, 125 g, or 140 g, because volume-to-weight conversion depends on the ingredient and sometimes on how enthusiastically somebody packed the cup. It should not rewrite an entire Polish recipe into natural English, decide that a visual section heading is not a cooking step, preserve a culturally meaningful name, and keep the converted temperature consistent everywhere it occurs.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That is no longer extraction. That is editorial normalization with consequences. A deterministic scraper is a very good postal worker; I was becoming annoyed that it would not also translate the letter, convert the measurements, remove the advertising, and check whether the instructions made sense. A wildly unfair performance review.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-fallback-that-never-fell-back&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The fallback that never fell back&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-fallback-that-never-fell-back&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The fallback that never fell back&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Mealie already has OpenAI integration. It also supports custom prompt files through &amp;lt;code&amp;gt;OPENAI_CUSTOM_PROMPT_DIR&amp;lt;/code&amp;gt;. Lovely. I wrote custom prompts, mounted them into the container, enabled an AI provider, and imported a normal recipe URL.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Nothing changed.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This was not an AI problem. It was an ordered list problem, the natural predator of engineering confidence.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;At the time of this deployment, Mealie&amp;amp;rsquo;s default scraper order was:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-python&amp;#34; data-lang=&amp;#34;python&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;DEFAULT_SCRAPER_STRATEGIES&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;RecipeScraperPackage&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;RecipeScraperOpenAITranscription&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;RecipeScraperOpenAI&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;RecipeScraperOpenGraph&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Mealie tries each strategy and returns the first usable result. &amp;lt;code&amp;gt;RecipeScraperPackage&amp;lt;/code&amp;gt; is the wrapper around &amp;lt;code&amp;gt;recipe-scrapers&amp;lt;/code&amp;gt;, and on most recipe pages it succeeds. That means the loop returns before &amp;lt;code&amp;gt;RecipeScraperOpenAI&amp;lt;/code&amp;gt; ever gets a turn. My magnificent custom scrape prompt was mounted correctly and being ignored with flawless reliability.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Fallbacks only happen when the first choice fails. A result can be technically successful and still not be the result you wanted. Distributed systems engineers know this as “Tuesday.”&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;So I applied a tiny, deeply impolite patch:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-python&amp;#34; data-lang=&amp;#34;python&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;DEFAULT_SCRAPER_STRATEGIES&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;RecipeScraperOpenAI&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;RecipeScraperPackage&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;RecipeScraperOpenAITranscription&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;RecipeScraperOpenGraph&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That is the whole behavioral change. OpenAI gets first refusal; the regular scraper remains as a fallback.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I call it monkey-patching because that communicates the correct level of shame, although technically it is a containerized file overlay. My OpenTofu creates a ConfigMap containing the patched &amp;lt;code&amp;gt;recipe_scraper.py&amp;lt;/code&amp;gt; and mounts it over the installed module inside Mealie&amp;amp;rsquo;s container:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;volume_mount&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; name&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;scraper-patch&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; mount_path&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;/opt/mealie/lib/python3.12/site-packages/mealie/services/scraper/recipe_scraper.py&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; sub_path&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;recipe_scraper.py&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; read_only&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The custom prompts are mounted separately under &amp;lt;code&amp;gt;/app/custom-prompts&amp;lt;/code&amp;gt;, and Mealie is pointed there with:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;OPENAI_CUSTOM_PROMPT_DIR=/app/custom-prompts&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Checksums of the environment, Secret, prompt data and Python patch live in the Deployment&amp;amp;rsquo;s pod-template annotations, so changing any of them causes Kubernetes to roll the pod. Infrastructure as code is wonderful because even my crimes against package ownership are reproducible.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There is an obvious maintenance bill: the patched file is copied from Mealie&amp;amp;rsquo;s installed version. Every Mealie upgrade now requires comparing that upstream module and verifying the path, imports and strategy interface still match. If upstream changes the scraper pipeline, my ConfigMap could replace new code with an old assumption and produce cuisine-flavoured sadness.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Do not cargo-cult this patch into production and then blame Python when an upgrade explodes. The repository README contains a large warning because future me deserves at least one witness.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;what-the-ai-path-actually-does&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;What the AI path actually does&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#what-the-ai-path-actually-does&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: What the AI path actually does&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The clever part is that Mealie does not ask the model to emit its internal database objects directly.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;RecipeScraperOpenAI&amp;lt;/code&amp;gt; strips the page into text, appends its JSON-LD blocks, finds a likely recipe image, and sends that material to the model with the &amp;lt;code&amp;gt;recipes.scrape-recipe&amp;lt;/code&amp;gt; prompt. The model returns a JSON representation of a schema.org &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt;. Mealie then wraps that JSON in a tiny fake HTML document as an &amp;lt;code&amp;gt;application/ld+json&amp;lt;/code&amp;gt; script and passes it back through the existing package scraper and cleaner.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;web page&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; visible text + JSON-LD + likely image&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; GPT-5.6 Luna&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; JSON representation of a schema.org Recipe&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; synthetic &amp;amp;lt;script type=&amp;amp;#34;application/ld+json&amp;amp;#34;&amp;amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; existing recipe-scrapers/Mealie cleaning path&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; stored recipe&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;It is an adapter wearing a fake moustache so the mature import pipeline accepts the model&amp;amp;rsquo;s output. I mean that affectionately. Reusing the existing validation and cleaning path is much safer than creating a second route into the database just because the words “AI integration” have entered the meeting.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;choosing-luna-instead-of-hiring-the-sun&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Choosing Luna instead of hiring the Sun&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#choosing-luna-instead-of-hiring-the-sun&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Choosing Luna instead of hiring the Sun&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I used &amp;lt;a href=&amp;#34;https://developers.openai.com/api/docs/models/gpt-5.6-luna&amp;#34;&amp;gt;GPT-5.6 Luna&amp;lt;/a&amp;gt;, the fastest and least expensive member of the GPT-5.6 family.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The family has three tiers:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ul&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Sol&amp;lt;/strong&amp;gt; is the flagship: the most capable option for hard coding, scientific, cyber and long-running agentic work.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Terra&amp;lt;/strong&amp;gt; balances capability and cost for everyday professional work.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Luna&amp;lt;/strong&amp;gt; is the fastest, lowest-cost tier.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ul&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;At launch, &amp;lt;a href=&amp;#34;https://openai.com/index/gpt-5-6/&amp;#34;&amp;gt;OpenAI priced GPT-5.6&amp;lt;/a&amp;gt; per million tokens at $5 input / $30 output for Sol, $2.50 / $15 for Terra, and $1 / $6 for Luna. The ratios are wonderfully easy: for the same token profile, Terra costs 2.5 times Luna and Sol costs five times Luna.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Sol would be spectacular overkill here. I am not asking the model to prove a theorem, coordinate agents across a week-long migration, or discover a novel compiler vulnerability. I am asking it to look at &amp;lt;code&amp;gt;8 oz cream cheese&amp;lt;/code&amp;gt; and return roughly &amp;lt;code&amp;gt;225 g cream cheese&amp;lt;/code&amp;gt; without turning the author&amp;amp;rsquo;s life story into Step 4.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Terra would also do the job, but the workload does not justify paying 2.5 times more unless testing shows a meaningful quality difference. It did not. Recipe extraction is bounded, repetitive, strongly instructed, and produces structured output. Luna already has much more linguistic and reasoning ability than this task needs.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Using the biggest available model for every workload is not architecture. It is standing at the kitchen counter slicing chives with a rescue helicopter.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-prompts-became-the-product&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The prompts became the product&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-prompts-became-the-product&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The prompts became the product&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The &amp;lt;a href=&amp;#34;https://github.com/mealie-recipes/mealie/tree/mealie-next/mealie/services/openai/prompts&amp;#34;&amp;gt;default Mealie prompts&amp;lt;/a&amp;gt; are intentionally compact. Its stock &amp;lt;code&amp;gt;scrape-recipe.txt&amp;lt;/code&amp;gt; is essentially: extract recipe data from webpage content as schema.org &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt;, do not invent information, and return &amp;lt;code&amp;gt;{}&amp;lt;/code&amp;gt; when there is not enough data. The stock ingredient prompt adds sensible basics about order, ambiguity, ranges, multilingual grammar and common units.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Those defaults have to work for everyone. Mine only has to work for my kitchen, where I want English recipe text, metric measurements, Celsius ovens, exact unit abbreviations and no instruction step whose sole contribution is &amp;lt;code&amp;gt;Make the sauce&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I could not get that behavior from the default prompts because they never ask for it. Models are capable, not psychic. “Extract this recipe” does not secretly mean “apply Ellie&amp;amp;rsquo;s house style, but preserve the epistemological integrity of dumpling nomenclature.”&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The full custom prompts live with the deployment, and I have also published &amp;lt;a href=&amp;#34;https://gist.augustini.xyz/myceliatrix/e89a90923f31466ea3e6ee66ca3b6ad8/&amp;#34;&amp;gt;both prompts in their full, gloriously over-specified form&amp;lt;/a&amp;gt; for anyone who wants to read the actual incantations instead of my civilized summary. Their jobs break down like this.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h3 id=&amp;#34;prompt-one-scrape-and-normalize-the-whole-recipe&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Prompt one: scrape and normalize the whole recipe&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#prompt-one-scrape-and-normalize-the-whole-recipe&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Prompt one: scrape and normalize the whole recipe&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h3&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;scrape-recipe.txt&amp;lt;/code&amp;gt; is the large one. It tells Luna to:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ol&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Find the actual recipe.&amp;lt;/strong&amp;gt; Prefer the author&amp;amp;rsquo;s content over navigation, SEO text, advertising, comments, subscription prompts and related recipes. Deduplicate responsive layouts, print views and repeated metadata.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Return one schema.org Recipe object.&amp;lt;/strong&amp;gt; Unsupported fields are omitted, insufficient pages become &amp;lt;code&amp;gt;{}&amp;lt;/code&amp;gt;, and invented data is forbidden.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Normalize measurements sensibly.&amp;lt;/strong&amp;gt; US customary units become practical metric values, Fahrenheit becomes Celsius, but teaspoons, tablespoons, counts and package units remain when conversion would make the recipe worse.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Avoid laboratory cosplay.&amp;lt;/strong&amp;gt; &amp;lt;code&amp;gt;1 cup water&amp;lt;/code&amp;gt; can become about &amp;lt;code&amp;gt;240 ml&amp;lt;/code&amp;gt;; it must not become &amp;lt;code&amp;gt;236.588 ml&amp;lt;/code&amp;gt;, because my measuring jug has not completed a metrology doctorate.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Use a unit house style.&amp;lt;/strong&amp;gt; Metric units become &amp;lt;code&amp;gt;mg&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;g&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;kg&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;ml&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;l&amp;lt;/code&amp;gt;; kitchen units become &amp;lt;code&amp;gt;tsp&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;tbsp&amp;lt;/code&amp;gt;. No &amp;lt;code&amp;gt;grams&amp;lt;/code&amp;gt; in one ingredient, &amp;lt;code&amp;gt;g&amp;lt;/code&amp;gt; in the next, and &amp;lt;code&amp;gt;grammes&amp;lt;/code&amp;gt; arriving later with a fake passport.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Keep countable things countable.&amp;lt;/strong&amp;gt; &amp;lt;code&amp;gt;4 cloves garlic&amp;lt;/code&amp;gt; remains natural language. It does not become &amp;lt;code&amp;gt;4 units garlic cloves&amp;lt;/code&amp;gt;, a phrase only a procurement database could love.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Clean the instructions.&amp;lt;/strong&amp;gt; Every step needs a concrete cooking action. Heading-only steps are removed or merged into the following action. Temperatures and repeated quantities must agree with the normalized ingredient list.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Translate into natural English.&amp;lt;/strong&amp;gt; Titles, descriptions, ingredients and instructions are translated, while proper nouns, brands, place names and culturally specific dish names survive when translation would reduce precision.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Choose the food image.&amp;lt;/strong&amp;gt; Prefer the main finished-dish image, reject logos, portraits, ads, pixels and decorative nonsense, and return one absolute URL.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Audit itself.&amp;lt;/strong&amp;gt; The prompt ends with a validation checklist covering ingredient order, unit forms, practical quantities, action-only instructions, image URLs and unsupported claims.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ol&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That final checklist mattered more than I expected. Long prompts are not automatically good prompts; often they are merely documentation that bills by the token. But explicit invariants gave Luna something concrete to verify before returning the JSON.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h3 id=&amp;#34;prompt-two-parse-ingredients-without-eating-any&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Prompt two: parse ingredients without eating any&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#prompt-two-parse-ingredients-without-eating-any&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Prompt two: parse ingredients without eating any&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h3&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;After importing, Mealie can parse ingredient strings into structured quantity, unit, food and note fields. My &amp;lt;code&amp;gt;parse-recipe-ingredients.txt&amp;lt;/code&amp;gt; prompt treats that as a lossless transformation:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;amp;#34;2 tbsp olive oil, plus more for frying&amp;amp;#34;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;quantity: 2&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;unit: tbsp&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;food: olive oil&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;note: plus more for frying&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Its most important rule is one input ingredient in, one structured ingredient out, in exactly the same order. Never merge, split, remove, reorder or invent ingredients. If a fragment is uncertain, preserve it in the note instead of confidently throwing it into the void.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The rest handles vulgar fractions, mixed numbers, written numbers, ranges, grouped quantities such as &amp;lt;code&amp;gt;2 dozen&amp;lt;/code&amp;gt;, canonical units, preparation notes, multilingual grammar and ambiguous tokens. It explicitly separates recognition from output: the model may understand &amp;lt;code&amp;gt;tablespoons&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;tbs&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;tbsp&amp;lt;/code&amp;gt;, but the stored unit must be exactly &amp;lt;code&amp;gt;tbsp&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This distinction sounds fussy until shopping-list aggregation meets &amp;lt;code&amp;gt;tablespoon&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;tablespoons&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;tbsp&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;T&amp;lt;/code&amp;gt;. Normalization is how four aliases stop pretending to be four pantry items.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The two prompts deliberately have different language policies. Whole-recipe scraping translates non-English recipes into English. Ingredient parsing preserves its source language unless translation was explicitly requested, because it may be invoked independently on an existing recipe. Context matters. Apparently even dinner needs an API contract.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;eight-commits-one-afternoon-several-unit-related-opinions&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Eight commits, one afternoon, several unit-related opinions&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#eight-commits-one-afternoon-several-unit-related-opinions&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Eight commits, one afternoon, several unit-related opinions&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The prompts did not work perfectly on the first attempt because nothing works perfectly on the first attempt except &amp;lt;code&amp;gt;rm -rf&amp;lt;/code&amp;gt;, and that works much too well.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I tested against recipes from different sites, languages and formatting traditions. Then I inspected what Mealie actually stored, found a new species of nonsense, tightened an invariant, and tried again. The deployment history records eight prompt-and-scraper refinements on July 13 alone.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Early versions said “convert to metric” but did not constrain the final vocabulary, so long unit names still leaked through. Then I required abbreviations. The model produced heading-only instruction steps, so the prompt gained action tests and merge rules. Images needed stricter selection. Counts needed protection from over-normalization. Translation needed an explicit boundary around culturally meaningful names. Ingredient parsing needed stronger lossless guarantees.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Prompt engineering, at least the useful kind, looks less like wizardry and more like property-based testing conducted by an increasingly opinionated cook:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;invariant: ingredient_count(output) == ingredient_count(input)&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;invariant: every instruction contains a cooking action&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;invariant: Fahrenheit does not survive normalization&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;invariant: supported metric units belong to {mg, g, kg, ml, l}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;invariant: culturally meaningful names are not translated into oatmeal&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The goal was not to describe every possible recipe on Earth. The goal was to identify failures I actually observed and convert them into rules precise enough to test on the next import.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-bill-featuring-an-anticlimactic-amount-of-capitalism&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The bill, featuring an anticlimactic amount of capitalism&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-bill-featuring-an-anticlimactic-amount-of-capitalism&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The bill, featuring an anticlimactic amount of capitalism&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;&amp;lt;img src=&amp;#34;https://augustini.wtf/fieldnotes/hacking-mealie-for-fun-and-dinner/openai-usage_hu_30a3bec08f2f8a6e.webp&amp;#34; alt=&amp;#34;OpenAI platform usage after prompt testing and importing roughly thirty recipes: 360,724 tokens, 73 requests and $0.73 spent.&amp;#34; width=&amp;#34;1024&amp;#34; height=&amp;#34;136&amp;#34; loading=&amp;#34;lazy&amp;#34; decoding=&amp;#34;async&amp;#34;&amp;gt;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;After all the prompt testing and importing roughly thirty recipes, the OpenAI platform reported:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ul&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;73 requests&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;360,724 total tokens&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;$0.73 spent&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;0 blocked requests&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ul&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That is about one US cent per request across the entire messy test session, or roughly two and a half cents per finished recipe if I unfairly charge all prompt-development traffic to the thirty recipes. Future imports should be cheaper because I am no longer repeatedly asking the model whether &amp;lt;code&amp;gt;millilitres&amp;lt;/code&amp;gt; means &amp;lt;code&amp;gt;ml&amp;lt;/code&amp;gt; and then updating a text file when it gets creative.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;With the same token mix, Terra would have cost about $1.83 and Sol about $3.65. None of those totals would endanger the household economy, but cost discipline is a habit. The correct question is not “can I afford the flagship?” It is “does the flagship improve this workload enough to justify five times the price?”&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;For parsing recipes, Luna&amp;amp;rsquo;s answer was already good. Sol cannot make 190°C more Celsius.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-aha-moment&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The aha moment&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-aha-moment&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The aha moment&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The original scraper and the model are not really competitors. They solve different layers of the problem.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;&amp;lt;code&amp;gt;recipe-scrapers&amp;lt;/code&amp;gt; extracts what a page says. Luna applies a house policy to what it means. Mealie&amp;amp;rsquo;s existing cleaner then converts the result into application data. The small Python patch merely changes which specialist speaks first.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That layering is why the result feels much better than replacing everything with “AI.” Deterministic code still fetches pages, controls the strategy pipeline, validates schema-shaped output, cleans fields and stores data. The model handles the fuzzy boundary where language, culture and inconsistent measurements make rigid parsers miserable.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The lesson is not that every scraper needs an LLM. Most do not. If you need faithful extraction from known sites, &amp;lt;code&amp;gt;recipe-scrapers&amp;lt;/code&amp;gt; is faster, cheaper and more predictable. My requirement was different: translate, normalize, preserve meaning, reconcile repeated values, and enforce a personal editorial standard across many sources. That is precisely where a language model earns its tiny invoice.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-post-mortem&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The post-mortem&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-post-mortem&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The post-mortem&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;My Mealie library now contains food from the places I wanted to explore, with English instructions, sensible metric quantities, Celsius temperatures, useful images and ingredients that can participate in shopping lists without developing multiple identities.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The final system is wonderfully disproportionate:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ul&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Mealie runs as a single restricted Kubernetes pod.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;CloudNativePG stores the database.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Authentik handles OIDC.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;OpenTofu mounts custom prompts and one patched Python module through ConfigMaps.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;GPT-5.6 Luna turns hostile food-blog archaeology into a normalized JSON representation of schema.org &amp;lt;code&amp;gt;Recipe&amp;lt;/code&amp;gt; data.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;The regular scraper remains available when the AI path cannot help.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ul&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Would a notebook have been simpler? Yes. A notebook also refuses to translate a Serbian recipe, convert the oven temperature, build a shopping list and roll itself after a prompt checksum changes. Checkmate, paper.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The patch is small. The prompts are not. That is the real shape of this project: six rearranged lines of Python opened the route, but most of the engineering went into defining what “a clean recipe” actually means.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;If you try something similar, start with the cheapest capable model, test on hostile real-world inputs, write down invariants instead of vibes, and keep deterministic validation on both sides of the model. Also leave yourself a very loud upgrade warning when you mount a ConfigMap over somebody else&amp;amp;rsquo;s Python package.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Future you will already be maintaining Kubernetes for a recipe manager. Be kind to her.&amp;lt;/p&amp;gt;&amp;#xA;</content:encoded></item><item><title>The pink keyboard firmware incident</title><link>https://augustini.wtf/fieldnotes/the-pink-keyboard-firmware-incident/</link><pubDate>Sun, 21 Jun 2026 02:32:21 +0200</pubDate><guid isPermaLink="true">https://augustini.wtf/fieldnotes/the-pink-keyboard-firmware-incident/</guid><dc:creator>Ellie Augustini</dc:creator><category>qmk</category><category>firmware</category><category>keyboard</category><category>embedded</category><category>debugging</category><category>iso</category><description>&amp;lt;p&amp;gt;Buying a keyboard because it is pink is a perfectly reasonable engineering decision. Anyone who disagrees has never spent eight hours a day communicating with a computer through a beige rectangle designed by a committee that thought &amp;amp;ldquo;texture&amp;amp;rdquo; meant &amp;amp;ldquo;slightly different plastic sadness.&amp;amp;rdquo;&amp;lt;/p&amp;gt;</description><content:encoded>&amp;lt;p&amp;gt;Buying a keyboard because it is pink is a perfectly reasonable engineering decision. Anyone who disagrees has never spent eight hours a day communicating with a computer through a beige rectangle designed by a committee that thought &amp;amp;ldquo;texture&amp;amp;rdquo; meant &amp;amp;ldquo;slightly different plastic sadness.&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;So there I was at Gliched, near Elgiganten, doing the responsible adult thing: impulse-purchasing a NuPhy Halo75 V2 because it was pink, cute, and I needed a new keyboard. That is not a supply-chain event. That is self-care with USB-C.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Then I did what any normal person would do after buying a keyboard: I went to the manufacturer&amp;amp;rsquo;s website, downloaded the firmware for the model I owned, flashed it, and expected my keys to continue being keys.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This was my first mistake. My second mistake was assuming &amp;amp;ldquo;firmware for this model&amp;amp;rdquo; included the ISO version of this model. My third mistake was having hope, which remains the most dangerous undefined behavior in consumer electronics.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The firmware flashed fine. The keyboard booted. The RGB did its little capitalism rainbow. And then the ISO keys were dead.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Not metaphorically dead. Actually dead. The useful Scandinavian/European bits of the board, the keys that make an ISO keyboard an ISO keyboard instead of an ANSI board wearing a taller Enter key as a disguise, simply stopped participating in society. The &amp;lt;code&amp;gt;&amp;amp;lt; &amp;amp;gt;&amp;lt;/code&amp;gt; key, the section/sign key, the extra punctuation choreography, all gone sideways.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I had taken a working keyboard and converted it into a very expensive pink argument.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-incident-report&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The incident report&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-incident-report&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The incident report&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;NuPhy published firmware for the Halo75 V2, but the downloadable build was effectively for ANSI. Flash that onto an ISO board and the matrix does exactly what firmware always does: precisely the wrong thing, with confidence.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;I asked NuPhy for the ISO firmware. They could not provide it.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is the part where the story should end with a support link, a zip file, and maybe a mild amount of shame about flashing vendor firmware like it came from a reliable civilization. Instead, the trail led to an abandoned-looking GitHub repository containing NuPhy&amp;amp;rsquo;s QMK source for the ANSI version.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The good news: source code existed.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The bad news: it was old QMK source, vendor-shaped, copied across variants, and written in the dialect of embedded C where every file has three jobs, six globals, and at least one function name that looks like it was translated through a fax machine.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The worse news: I was now emotionally invested.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;On June 16, 2026, the first commit landed on the &amp;lt;code&amp;gt;nuphy-keyboards&amp;lt;/code&amp;gt; branch:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;3064e8c506f Start working on ISO version of the halo75 v2 and add woodpecker&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That commit was not a delicate edit. It added a full ISO variant: keyboard metadata, keymaps, LED tables, RF code, side LED code, sleep handling, VIA support, and CI. About 4,800 lines of &amp;amp;ldquo;I bought this keyboard yesterday and now apparently I maintain it.&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;At this point, the problem looked simple in the same way a leaking pipe looks simple before you learn the wall is load-bearing.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The ISO board needed:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ul&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;the right matrix positions,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;the right ISO keycodes,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;the right VIA definition,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;the right LED map,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;and the firmware to stop treating Nordic punctuation like optional DLC.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ul&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;So I fixed keymaps. Then fixed keymaps again. Then fixed RGB. Then fixed an invalid matrix position. Then did some more layout fixes. The commit log from June 16 is basically firmware debugging as a cardio exercise:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix invalid matrix position&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Maybe keyfixes for iso?&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix rgb&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix keymaps&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix keymaps&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix keymaps&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Minior layout fixes&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Yes, &amp;amp;ldquo;Minior.&amp;amp;rdquo; The typo is part of the archaeological record. Leave it in the museum.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-rabbit-hole&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The rabbit hole&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-rabbit-hole&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The rabbit hole&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The original NuPhy source was based around QMK 0.25.7-ish. Current upstream QMK had moved on to 0.33.7. That does not sound like a huge jump if you think version numbers are decorative. In firmware land, it means APIs moved, config schemas changed, old aliases disappeared, and whatever worked before now has the social stability of a JavaScript build tool after a minor release.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;On June 18, the branch became &amp;lt;code&amp;gt;nuphy-on-current&amp;lt;/code&amp;gt;, and this happened:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;977cf375c51 Port nuphy keyboards onto QMK 0.33.7; strip non-nuphy keyboards&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That was the forklift upgrade. The old NuPhy code got dragged onto modern QMK, and the repo was stripped down so it was no longer carrying the entire keyboard directory like a cursed shipping container.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The migration broke in all the ways you would expect and several ways you would not unless you have personally offended an STM32.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;QMK had renamed metadata expectations. Some keyboards had &amp;lt;code&amp;gt;keyboard.json&amp;lt;/code&amp;gt;; newer QMK wanted &amp;lt;code&amp;gt;info.json&amp;lt;/code&amp;gt; in the right places. RGB matrix coordinates needed to validate as integers. Old RGB keycodes had been deprecated, so the firmware needed modern &amp;lt;code&amp;gt;RM_*&amp;lt;/code&amp;gt; names instead of legacy &amp;lt;code&amp;gt;RGB_*&amp;lt;/code&amp;gt; aliases. EEPROM datablock functions had changed shape. GPIO helpers had changed names. ChibiOS configuration needed attention.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;None of this is glamorous. This is the work where a compiler looks at you and says, &amp;amp;ldquo;I see you brought me 2024 code. Unfortunately, this is 2026, and I have developed standards.&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Then came the proper embedded bugs, the ones that do not politely fail at compile time.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-keyboard-that-typed-from-the-wrong-pins&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The keyboard that typed from the wrong pins&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-keyboard-that-typed-from-the-wrong-pins&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The keyboard that typed from the wrong pins&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The best bug was the UART pin conflict.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The Halo75 V2 uses an STM32F072 talking to an nRF wireless module. The keyboard matrix also uses MCU pins. This is normal until the firmware configures the wrong pins for UART and accidentally turns matrix columns into a serial peripheral.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That is not &amp;amp;ldquo;a bug.&amp;amp;rdquo; That is a séance with scan codes.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The QMK UART driver defaulted to pins that were fine in some universe, just not this one. On this board, USART1 needed PB6/PB7. Without overriding the UART TX/RX pins, the driver defaulted to A9/A10, which were matrix columns 13 and 14. So when &amp;lt;code&amp;gt;rf_uart_init()&amp;lt;/code&amp;gt; ran, it reconfigured keyboard matrix pins as USART pins, and the keyboard started inventing input like it had discovered improv.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The fix landed at 05:15 on June 19:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;dea005ec792 Fix Nuphy Halo75 V2 ISO: LED matrix, typing, and UART pin conflict&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That commit brought the board back toward reality: restore the IS31FL3733 compatibility macros, fix I2C addresses, re-enable the RF housekeeping path, bring the RGB matrix power rail up at the right point, restore DMA config, add missing LED positions, and force the UART pins to the board&amp;amp;rsquo;s actual hardware.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is the part of firmware work where the abstraction stack politely excuses itself and you are left reading datasheets like a medieval monk with a logic analyzer.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Then Bluetooth still did not work.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The keyboard would show the pairing blink locally, but nothing appeared in the Bluetooth device list. That is a special kind of insult: the UI says &amp;amp;ldquo;pairing,&amp;amp;rdquo; the radio says nothing, and your laptop says &amp;amp;ldquo;new phone who dis.&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The next commit found it:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;49352a2a3ac Fix RF/Bluetooth: set UART_TX/RX_PAL_MODE = 0 for STM32F072&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;On STM32F072, USART1 on PB6/PB7 uses alternate function 0. QMK&amp;amp;rsquo;s default UART alternate-function mode was 7, appropriate for STM32F4-style expectations, but wrong here. AF7 on those pins was not USART1. The nRF module was never receiving the command to advertise. The keyboard was blinking into the void.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Firmware debugging is mostly asking, &amp;amp;ldquo;is the hardware lying, is the code lying, or did I accidentally configure a comparator output and call it Bluetooth?&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The answer, as usual, was yes.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-led-side-quest-because-of-course-there-was-one&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The LED side quest, because of course there was one&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-led-side-quest-because-of-course-there-was-one&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The LED side quest, because of course there was one&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Once the keyboard typed, the LEDs demanded legal representation.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The Halo75 V2 has 128 LEDs driven by IS31FL3733 chips. The main keys, status indicators, battery/system lights, and side/rim LEDs all share the stage. This is fine until RGB animations decide the entire LED array belongs to them and overwrite status indicators like a startup founder overwriting a database with a Notion export.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The commit log from late June 18 into early June 19 reads like someone negotiating with a disco:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix side LED bugs so it leaves status leds alone&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix rim animations overwriting status LEDs&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix custom RGB matrix effects overwriting status LEDs&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Fix startup animation power_play_index bound&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Slow down startup animation timing for smoother effect&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Skip status LED indices in startup animation to prevent interference&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Prevent animations from turning off status LEDs&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;Add rgb_matrix_indicators_user hook to restore status LEDs after built-in effects&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The architectural fix was to separate responsibility. QMK RGB matrix effects can draw the main keyboard lighting, but status and side LEDs need to win at the end of the frame. Otherwise the keyboard loses the ability to tell you useful things like battery, mode, and &amp;amp;ldquo;please stop turning my indicators into vaporwave.&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Later, that turned into a cleaner shared implementation:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;776478e78ab Major refactor, share the code between ansi and iso as much as possible&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Before that refactor, ANSI and ISO carried duplicate copies of RF code, side LED code, sleep code, headers, and config. That is how you grow two firmware variants that look related but fail in different accents. The refactor pulled shared behavior up into &amp;lt;code&amp;gt;keyboards/nuphy/halo75_v2/&amp;lt;/code&amp;gt;, leaving the variants to describe what is actually different: physical layout, keymaps, and LED zoning.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The result was smaller, less duplicated, and less likely to require fixing the same UART bug twice. A low bar, yes, but embedded firmware is built on respecting low bars. They are usually connected to ground.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-performance-pass&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The performance pass&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-performance-pass&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The performance pass&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;After the board typed and lit up, it still felt sluggish.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There is a very specific humiliation in making a boutique mechanical keyboard feel worse than a three-dollar office keyboard that has seen things in a municipal procurement closet. The Halo75 V2 had side LED work, RF housekeeping, UART command handling, sleep handling, and RGB animation all sharing a tiny real-time budget. Blocking waits in hot-ish paths are how you turn &amp;amp;ldquo;firmware&amp;amp;rdquo; into &amp;amp;ldquo;firm maybe later.&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;So June 19 turned into a latency cleanup:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;cf1f86e9329 Remove blocking waits from hot-ish paths&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;8f1c1a0b4d7 Cap rgb animations at approx. 60 fps&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;1b5728b8298 Coalesce/drop low-priority deferred UART commands&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;0314ae48b22 Harden RF parser and make device reset non-blocking&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;1b9638856b3 Remove dead ack-wait from uart_send_cmd, convert sleep callers to deferred&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The idea was straightforward: keep the main loop moving. Do not block the scan path waiting for decorative work. Do not make a factory reset freeze the firmware for over a second. Do not let low-priority UART chatter pile up while key reports are trying to leave the building.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is the point where &amp;amp;ldquo;keyboard firmware&amp;amp;rdquo; stops being a keymap and becomes a tiny cooperative scheduler with lighting and emotional baggage.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The RF parser also got stricter. Instead of trusting arbitrary packet lengths and setting ACK/sync state too early, it started validating command-specific lengths and resetting RX state on parse exits. In C, optimism is not a strategy. It is how you spend Saturday night discovering that one stale byte can become a lifestyle.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-hid-console-plot-twist&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The HID console plot twist&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-hid-console-plot-twist&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The HID console plot twist&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Then came the weird one: Cmd+V behaved strangely.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;At this stage, it was reasonable to suspect ghosting. The keyboard had already had a real UART/matrix pin conflict. The firmware had just been ported across years of QMK changes. There were RF paths, NKRO reports, modifier state, layers, debounce, and a physical matrix all involved. Plenty of suspects. Nobody had an alibi.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;So I added a debug console:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;0de414bd565 debug: add matrix/report debug console for input diagnostics&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;It can be enabled like this:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-sh&amp;#34; data-lang=&amp;#34;sh&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;qmk flash -kb nuphy/halo75_v2/iso -km via -e &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;CONSOLE_ENABLE&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;yes&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The console logs raw matrix rows, debounced rows, per-column masks, key events, layers, mods, weak mods, oneshot mods, and the row state at the moment each event fires. It is intentionally cheap: compare &amp;lt;code&amp;gt;MATRIX_ROWS&amp;lt;/code&amp;gt;, skip output if nothing changed, allocate nothing, and print only when useful.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is exactly the kind of tool you write when you are convinced the machine is gaslighting you and you would like timestamps for the deposition.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;And then the console proved the keyboard was innocent.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The &amp;amp;ldquo;ghosting&amp;amp;rdquo; was me.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Because of nerve damage in my left hand, I was accidentally bumping Ctrl without feeling it. So Cmd+V was sometimes not Cmd+V in the clean little mental model where fingers are reliable peripherals. The firmware was fine. The matrix was fine. The report was fine. The bug was an accessibility mismatch between switch resistance and sensory feedback.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That is the kind of debugging revelation that makes you stare at the ceiling for a moment.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Hours of firmware investigation. A custom HID console. Matrix masks. Modifier reports. RF paranoia. And the fix was not a bitmask.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The fix was replacing the Ctrl switch with one that has higher resistance, so I can feel when I bump it.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Honestly? That is beautiful. Annoying, but beautiful.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Software people love pretending all bugs live in software. Embedded work cures you of that disease. The system includes the PCB, the MCU, the wireless module, the firmware, the operating system, the desk, the key switch, and the human hand attached to the debugging session. Ignore any layer and it will eventually file a bug report in production.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-post-mortem&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The post-mortem&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-post-mortem&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The post-mortem&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;By June 20, the branch had reached:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;d1fe40f3bcc Minior bugfix&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The repository is public, if you want to inspect the suffering in its natural habitat: &amp;lt;a href=&amp;#34;https://code.augustini.xyz/myceliatrix/qmk_firmware&amp;#34;&amp;gt;myceliatrix/qmk_firmware&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;From June 16 to June 20, the firmware went from &amp;amp;ldquo;I would like my ISO keys back&amp;amp;rdquo; to a modern QMK 0.33.7 port with:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ul&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;ISO and ANSI variants,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;shared Halo75 V2 firmware code,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;VIA-compatible keymaps,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;fixed ISO matrix/key positions,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;working LED matrix and side LED zoning,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;IS31FL3733 compatibility fixes,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;UART pin and alternate-function fixes for STM32F072,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;RF/Bluetooth housekeeping restored,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;USB suspend behavior fixed for wireless mode,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;fewer blocking waits,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;a stricter RF parser,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;deferred UART command handling,&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;and a HID console that found a human factors bug wearing a firmware costume.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ul&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is why &amp;amp;ldquo;just flash the firmware&amp;amp;rdquo; is a cursed phrase. Firmware is not an app update. Firmware is a tiny treaty between silicon, timing, voltage, physical switches, protocol state, and somebody&amp;amp;rsquo;s hand at 01:07 in the morning.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The lesson is not &amp;amp;ldquo;never buy a pink keyboard impulsively.&amp;amp;rdquo; That would be cowardice, and also aesthetically wrong.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The lesson is: if a device ships with QMK, source matters. If the vendor only publishes one layout&amp;amp;rsquo;s firmware, layout matters. If an old firmware tree works, the exact QMK version matters. If wireless goes quiet, alternate functions matter. If keys appear haunted, matrix logs matter. And if your body is part of the input system, switch feel matters too.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Also, before assuming your firmware is broken, maybe check whether your Ctrl key is being lovingly shoulder-checked by a hand with partial sensation.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;But still write the debug console.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Future you deserves receipts.&amp;lt;/p&amp;gt;&amp;#xA;</content:encoded></item><item><title>How this site works: a tiny static site in a surprisingly serious trenchcoat</title><link>https://augustini.wtf/fieldnotes/how-this-site-works/</link><pubDate>Sun, 07 Jun 2026 14:00:00 +0200</pubDate><guid isPermaLink="true">https://augustini.wtf/fieldnotes/how-this-site-works/</guid><dc:creator>Ellie Augustini</dc:creator><category>hugo</category><category>infrastructure</category><category>talos</category><category>opentofu</category><category>ci-cd</category><category>security</category><category>kubernetes</category><category>self-hosting</category><description>&amp;lt;p&amp;gt;This website looks like a small terminal window that wandered into a pastel cabinet. That is the fun part. The less visible part is that it is built like a production service: reproducible static build, minimal container, private registry, pinned deployments, restricted Kubernetes namespace, Gateway API routes, and OpenTofu state living in a Rook Ceph object store.&amp;lt;/p&amp;gt;</description><content:encoded>&amp;lt;p&amp;gt;This website looks like a small terminal window that wandered into a pastel cabinet. That is the fun part. The less visible part is that it is built like a production service: reproducible static build, minimal container, private registry, pinned deployments, restricted Kubernetes namespace, Gateway API routes, and OpenTofu state living in a Rook Ceph object store.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is perhaps excessive for a personal website. That is also the point.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;A static site is one of the nicest places to practice infrastructure discipline because the application itself is brutally simple. There is no database migration trying to eat your evening. No websocket fleet. No &amp;amp;ldquo;just one background worker&amp;amp;rdquo; that secretly wants to become a platform. The whole runtime requirement is: please serve these files over HTTP without doing anything cursed.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;So this post is a tour of the whole thing. Not just the Hugo bits in this repository, but also the deployment estate in &amp;lt;code&amp;gt;/Users/ellie/talos/opentofu-apps/augustini-wtf&amp;lt;/code&amp;gt;, because that repo is the other half of the creature. One repo makes the artifact. The other repo tells the cluster how to run it.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-shape-of-the-system&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The shape of the system&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-shape-of-the-system&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The shape of the system&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There are two repositories involved:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ul&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;code&amp;gt;augustini.wtf&amp;lt;/code&amp;gt;: the website source, Hugo theme, Tailwind source CSS, Woodpecker pipeline, and scratch-image Dockerfile.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;&amp;lt;code&amp;gt;talos/opentofu-apps/augustini-wtf&amp;lt;/code&amp;gt;: the OpenTofu workspace that creates Kubernetes resources and Gateway API routes for the site.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ul&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The deployment path looks like this:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;git push&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Woodpecker builds Hugo site&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Tailwind emits the final theme CSS&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Hugo writes public/&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Docker Buildx packages public/ with static-web-server&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Harbor receives latest and a timestamp tag&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Woodpecker updates Terrakube variable image_tag&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Terrakube runs the OpenTofu workspace&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Kubernetes Deployment rolls to the pinned image&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; -&amp;amp;gt; Gateway API sends augustini.wtf traffic to the Service&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The design goal is boring in the good way: the website repo should not need cluster credentials, my laptop should not be the deployment authority, and Kubernetes should not run an image named &amp;lt;code&amp;gt;latest&amp;lt;/code&amp;gt; while everyone politely pretends that is a version.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;hugo-the-part-that-makes-pages&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Hugo: the part that makes pages&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#hugo-the-part-that-makes-pages&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Hugo: the part that makes pages&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The site is Hugo with a local theme named &amp;lt;code&amp;gt;techprincess&amp;lt;/code&amp;gt;. The root &amp;lt;code&amp;gt;hugo.toml&amp;lt;/code&amp;gt; is intentionally conventional:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-toml&amp;#34; data-lang=&amp;#34;toml&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;baseURL&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;https://augustini.wtf/&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;title&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;Ellie Augustini&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;theme&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;techprincess&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;enableRobotsTXT&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kc&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;summaryLength&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;24&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Content lives under &amp;lt;code&amp;gt;content/&amp;lt;/code&amp;gt;: home, about, fieldnotes, projects, and individual project/fieldnote pages. Hugo&amp;amp;rsquo;s content model does most of the routing without ceremony. A post under &amp;lt;code&amp;gt;content/fieldnotes/foo.md&amp;lt;/code&amp;gt; becomes a fieldnote. A project under &amp;lt;code&amp;gt;content/projects/foo.md&amp;lt;/code&amp;gt; becomes a project page. The theme layouts decide how those sections feel.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The theme is very much not a generic &amp;amp;ldquo;minimal fieldnotes&amp;amp;rdquo; theme. It is a terminal UI cosplay machine, but with the useful parts kept and the fake-terminal nonsense mostly avoided. The base layout wraps the page in a bordered panel with a title bar, three colored dots, a scanline overlay, and then regular semantic HTML inside it:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-html&amp;#34; data-lang=&amp;#34;html&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;body&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;min-h-screen bg-background text-foreground font-mono antialiased&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;a&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;skip-link&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;href&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;#main&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;...&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;a&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;div&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;...&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;div&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;... border border-border bg-card ...&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;div&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;... border-b border-border bg-secondary/60 ...&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;span&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;... bg-pink&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;span&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;span&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;... bg-gold&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;span&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;span&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;... bg-mint&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;span&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;span&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;ellie@princess: ~&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;span&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;div&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;11&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;div&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;class&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;scanlines&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;12&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; {{ partial &amp;amp;#34;header.html&amp;amp;#34; . }}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;13&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;main&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;id&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;main&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;tabindex&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;-1&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;14&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; {{ block &amp;amp;#34;main&amp;amp;#34; . }}{{ end }}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;15&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;main&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;16&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;div&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;17&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;div&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;18&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; {{ partial &amp;amp;#34;footer.html&amp;amp;#34; . }}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;19&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;div&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;20&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;/&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;body&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;There is a real skip link. The main region is focusable. The terminal aesthetic is allowed to be cute, but it is not allowed to eat the accessibility basics.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-theme-one-font-many-little-constraints&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The theme: one font, many little constraints&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-theme-one-font-many-little-constraints&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The theme: one font, many little constraints&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The visual system is all Iosevka, all the time. The font files are self-hosted from &amp;lt;code&amp;gt;themes/techprincess/static/fonts/&amp;lt;/code&amp;gt;, so the finished site does not ask Google Fonts, Bunny, Adobe, or some random CDN for permission to render text.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The full Iosevka latin woff2 files are about a megabyte each, which is silly for a site whose pages are a few hundred kilobytes. After Hugo writes &amp;lt;code&amp;gt;public/&amp;lt;/code&amp;gt;, a small Python step (&amp;lt;code&amp;gt;scripts/subset-fonts.py&amp;lt;/code&amp;gt;, run via &amp;lt;code&amp;gt;npm run subset-fonts&amp;lt;/code&amp;gt;) walks every rendered &amp;lt;code&amp;gt;.html&amp;lt;/code&amp;gt;, collects the set of characters actually used, and runs &amp;lt;code&amp;gt;pyftsubset&amp;lt;/code&amp;gt; over each source font to produce a subset covering exactly those glyphs (plus a small safety set for the footer clock and CSS &amp;lt;code&amp;gt;content&amp;lt;/code&amp;gt; bullets). The source fonts stay untouched; only &amp;lt;code&amp;gt;public/fonts/&amp;lt;/code&amp;gt; is rewritten. The result is roughly 4 MB of fonts shrinking to about 195 KB total, with no visible difference.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Tailwind is used as a compiler and constraint system, not as a browser dependency. &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt; has the important scripts:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-json&amp;#34; data-lang=&amp;#34;json&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;{&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;&amp;amp;#34;scripts&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;{&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;&amp;amp;#34;build:css&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;tailwindcss -i themes/techprincess/assets/css/source.css -o themes/techprincess/assets/css/main.css --minify&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;&amp;amp;#34;subset-fonts&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;python3 scripts/subset-fonts.py&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;&amp;amp;#34;build&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;npm run build:css &amp;amp;amp;&amp;amp;amp; hugo --minify &amp;amp;amp;&amp;amp;amp; npm run subset-fonts&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;&amp;amp;#34;serve&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;npm run build:css &amp;amp;amp;&amp;amp;amp; hugo serve --disableFastRender&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The Tailwind config scans Hugo content and layouts, the theme JS, and both Hugo config files. It also enables Iconify&amp;amp;rsquo;s dynamic selectors so the templates can use classes like &amp;lt;code&amp;gt;icon-[lucide--heart]&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;icon-[simple-icons--forgejo]&amp;lt;/code&amp;gt; without manually vendoring SVGs into every partial.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The palette is expressed as OKLCH tokens in &amp;lt;code&amp;gt;source.css&amp;lt;/code&amp;gt;:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-css&amp;#34; data-lang=&amp;#34;css&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nd&amp;#34;&amp;gt;root&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;{&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--background&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.16&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.012&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;300&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--foreground&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.92&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.015&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;330&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--card&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.2&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.015&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;305&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--primary&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.8&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.16&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;350&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--accent&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.86&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.16&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;168&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--pink&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.82&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.17&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;352&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--mint&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.86&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.16&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;168&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--gold&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.87&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.15&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;92&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;--lilac&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.8&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mf&amp;#34;&amp;gt;0.12&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;300&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;11&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Tailwind maps those into named colors:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-js&amp;#34; data-lang=&amp;#34;js&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;colors&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;{&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;background&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;oklch(var(--background) / &amp;amp;lt;alpha-value&amp;amp;gt;)&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;foreground&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;oklch(var(--foreground) / &amp;amp;lt;alpha-value&amp;amp;gt;)&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;pink&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;oklch(var(--pink) / &amp;amp;lt;alpha-value&amp;amp;gt;)&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;mint&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;oklch(var(--mint) / &amp;amp;lt;alpha-value&amp;amp;gt;)&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;gold&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;oklch(var(--gold) / &amp;amp;lt;alpha-value&amp;amp;gt;)&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;lilac&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;oklch(var(--lilac) / &amp;amp;lt;alpha-value&amp;amp;gt;)&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;,&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That means the layouts can stay readable: &amp;lt;code&amp;gt;border-pink/60&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;text-muted-foreground&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;bg-secondary/50&amp;lt;/code&amp;gt;, and so on. No component library. No design token runtime. Just CSS variables, Tailwind&amp;amp;rsquo;s build step, and a strong preference for rectangles with manners.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;hugo-pipes-cache-busted-assets-without-a-bundler-shaped-monster&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Hugo pipes: cache-busted assets without a bundler-shaped monster&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#hugo-pipes-cache-busted-assets-without-a-bundler-shaped-monster&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Hugo pipes: cache-busted assets without a bundler-shaped monster&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The theme uses Hugo&amp;amp;rsquo;s asset pipeline for the final CSS and JS includes.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;In development, CSS is linked directly:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-go-html-template&amp;#34; data-lang=&amp;#34;go-html-template&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{-&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;with&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;resources&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;.Get&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;css/main.css&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{-&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;if&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;hugo&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;.IsDevelopment&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;link&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;rel&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;stylesheet&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;href&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;.RelPermalink&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{-&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;else&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{-&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;with&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;|&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;fingerprint&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;lt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nt&amp;#34;&amp;gt;link&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;rel&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;stylesheet&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;href&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;.RelPermalink&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;integrity&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;.Data.Integrity&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;na&amp;#34;&amp;gt;crossorigin&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;&amp;amp;#34;anonymous&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;gt;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{-&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;end&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{-&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;end&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;{{-&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;end&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cp&amp;#34;&amp;gt;}}&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;In production, Hugo fingerprints it and adds an integrity attribute. The JavaScript gets the same treatment after passing through &amp;lt;code&amp;gt;js.Build&amp;lt;/code&amp;gt; with minification enabled outside development.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The browser JS is intentionally tiny. It does two things:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ol&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Adds the &amp;lt;code&amp;gt;dark&amp;lt;/code&amp;gt; class to the document element.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Adds copy buttons to Chroma code blocks, stripping line number spans before writing text to the clipboard.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ol&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There is also a small inline footer clock and a privacy-aware loader for the site&amp;amp;rsquo;s self-hosted Umami analytics. The loader respects Do Not Track, Global Privacy Control and the local opt-out, while removing query strings and fragments before a page view leaves the browser. This means the live browser behavior is not zero JavaScript, but it is measured in teaspoons. There is no client router, hydration pass, state manager, session recorder, or marketing attribution ritual.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;syntax-highlighting-chroma-but-dressed-for-the-room&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Syntax highlighting: Chroma, but dressed for the room&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#syntax-highlighting-chroma-but-dressed-for-the-room&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Syntax highlighting: Chroma, but dressed for the room&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Hugo uses Chroma for syntax highlighting. The config keeps classes enabled:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-toml&amp;#34; data-lang=&amp;#34;toml&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;markup&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;highlight&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;noClasses&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kc&amp;#34;&amp;gt;false&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;style&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;monokai&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;lineNos&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kc&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;lineNumbersInTable&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kc&amp;#34;&amp;gt;false&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;nx&amp;#34;&amp;gt;tabWidth&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;mi&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The &amp;lt;code&amp;gt;style = &amp;#39;monokai&amp;#39;&amp;lt;/code&amp;gt; line mostly stops being the story once &amp;lt;code&amp;gt;noClasses = false&amp;lt;/code&amp;gt; is set. Hugo emits semantic token classes, and the theme CSS decides what those classes mean.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Keywords become mint. Types and tag names become pink. Functions become gold. Strings become lilac. Comments get shoved into the quiet corner at muted 60 percent opacity, as comments deserve. Line numbers are non-interactive, unselectable spans, which matters because copy buttons clone the code block and remove &amp;lt;code&amp;gt;.lnt&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;.ln&amp;lt;/code&amp;gt; before copying.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The result is syntax highlighting that belongs to the same visual world as the rest of the site. It does not look like somebody taped a Dracula code block onto a different website and fled the scene.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;woodpecker-from-source-tree-to-artifact&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Woodpecker: from source tree to artifact&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#woodpecker-from-source-tree-to-artifact&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Woodpecker: from source tree to artifact&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The CI pipeline is &amp;lt;code&amp;gt;.woodpecker/publish.yml&amp;lt;/code&amp;gt;. It runs on pushes and manual events.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The first step uses &amp;lt;code&amp;gt;node:22-bookworm-slim&amp;lt;/code&amp;gt;, because the build needs npm for Tailwind and Hugo for the static site. Hugo is installed explicitly from GitHub releases:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-sh&amp;#34; data-lang=&amp;#34;sh&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;hugo_version&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;0.162.1&amp;amp;#39;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;case&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;$(&amp;lt;/span&amp;gt;uname -m&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt; in&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; x86_64&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;hugo_arch&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;linux-amd64&amp;amp;#39;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; aarch64&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;|&amp;lt;/span&amp;gt;arm64&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;hugo_arch&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;linux-arm64&amp;amp;#39;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; *&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;)&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;nb&amp;#34;&amp;gt;echo&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;Unsupported CI architecture: &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;$(&amp;lt;/span&amp;gt;uname -m&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;&amp;amp;amp;&amp;lt;/span&amp;gt;2&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;nb&amp;#34;&amp;gt;exit&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;m&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;;;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;esac&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;curl -fsSL &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;https://github.com/gohugoio/hugo/releases/download/v&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;si&amp;#34;&amp;gt;${&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;hugo_version&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;si&amp;#34;&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;/hugo_extended_&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;si&amp;#34;&amp;gt;${&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;hugo_version&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;si&amp;#34;&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;_&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;si&amp;#34;&amp;gt;${&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;hugo_arch&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;si&amp;#34;&amp;gt;}&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;.tar.gz&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;|&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; tar -xz -C /usr/local/bin hugo&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Then:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-sh&amp;#34; data-lang=&amp;#34;sh&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;npm ci&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;npm run build&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nb&amp;#34;&amp;gt;test&amp;lt;/span&amp;gt; -f public/index.html&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;date -u +%Y%m%d%H%M%S &amp;amp;gt; .release_tag&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;nb&amp;#34;&amp;gt;printf&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s1&amp;#34;&amp;gt;&amp;amp;#39;latest,%s\n&amp;amp;#39;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;$(&amp;lt;/span&amp;gt;cat .release_tag&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;)&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;amp;gt; .tags&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That &amp;lt;code&amp;gt;.tags&amp;lt;/code&amp;gt; file is for the Docker plugin. Every build gets &amp;lt;code&amp;gt;latest&amp;lt;/code&amp;gt; for convenience and a UTC timestamp tag for deployment. The timestamp is the important one. &amp;lt;code&amp;gt;latest&amp;lt;/code&amp;gt; is a bookmark. The timestamp is a version.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-container-static-web-server-inside-scratch&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The container: static-web-server inside scratch&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-container-static-web-server-inside-scratch&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The container: static-web-server inside scratch&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The image build is wonderfully rude:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-dockerfile&amp;#34; data-lang=&amp;#34;dockerfile&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;FROM&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;ghcr.io/static-web-server/static-web-server:latest&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;AS&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;sws-binary&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;FROM&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;scratch&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;COPY&amp;lt;/span&amp;gt; --from&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;sws-binary /static-web-server /sws&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;COPY&amp;lt;/span&amp;gt; ./public /public&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;ENV&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;SERVER_PORT&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;m&amp;#34;&amp;gt;8080&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;ENV&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;nv&amp;#34;&amp;gt;SERVER_ROOT&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;/public&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;11&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;EXPOSE&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;w&amp;#34;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s&amp;#34;&amp;gt;8080&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;12&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;13&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;ENTRYPOINT&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;/sws&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;It copies the &amp;lt;code&amp;gt;static-web-server&amp;lt;/code&amp;gt; binary out of the upstream image, copies Hugo&amp;amp;rsquo;s &amp;lt;code&amp;gt;public/&amp;lt;/code&amp;gt; directory, and then runs from &amp;lt;code&amp;gt;scratch&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There is no shell. No package manager. No CA bundle unless the binary and app need one, which this site does not for serving local files. No busybox escape hatch. If someone gets arbitrary command execution inside this container, their prize is a filesystem containing a web server binary and some HTML. It is the infrastructure equivalent of opening a safe and finding a polite note that says &amp;amp;ldquo;no.&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Static Web Server listens on &amp;lt;code&amp;gt;8080&amp;lt;/code&amp;gt; and serves &amp;lt;code&amp;gt;/public&amp;lt;/code&amp;gt;. The site is pure static output, so it does not need an application runtime. The container is not where logic lives. The container is where files wait to be sent.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The &amp;lt;code&amp;gt;publish-image&amp;lt;/code&amp;gt; Woodpecker step uses &amp;lt;code&amp;gt;woodpeckerci/plugin-docker-buildx&amp;lt;/code&amp;gt;, pushes to &amp;lt;code&amp;gt;registry.augustini.xyz/sites/augustiniwtf&amp;lt;/code&amp;gt;, and targets &amp;lt;code&amp;gt;linux/amd64&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-handoff-woodpecker-pokes-terrakube-not-kubernetes&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The handoff: Woodpecker pokes Terrakube, not Kubernetes&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-handoff-woodpecker-pokes-terrakube-not-kubernetes&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The handoff: Woodpecker pokes Terrakube, not Kubernetes&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The deploy step is where the two repos shake hands.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Woodpecker does not run &amp;lt;code&amp;gt;kubectl apply&amp;lt;/code&amp;gt;. It does not carry a kubeconfig. It does not patch a Deployment directly. Instead, it calls the Terrakube API.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The script:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ol&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Reads &amp;lt;code&amp;gt;.release_tag&amp;lt;/code&amp;gt;.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Finds organization &amp;lt;code&amp;gt;v0cloud&amp;lt;/code&amp;gt;.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Finds workspace &amp;lt;code&amp;gt;augustini-wtf&amp;lt;/code&amp;gt;.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Finds Terraform variable &amp;lt;code&amp;gt;image_tag&amp;lt;/code&amp;gt;.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Patches that variable to the new timestamp.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Finds the template named &amp;lt;code&amp;gt;Plan and apply&amp;lt;/code&amp;gt;.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Creates a Terrakube job for that workspace/template pair.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ol&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is the most important boundary in the deployment. Woodpecker is allowed to produce an artifact and request a deployment. Terrakube is the thing allowed to evaluate and apply infrastructure.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That gives a cleaner audit trail: source build in Woodpecker, stateful infrastructure action in Terrakube, actual reconciliation in Kubernetes.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;After deployment, Woodpecker also prunes Harbor artifacts. It keeps five non-&amp;lt;code&amp;gt;latest&amp;lt;/code&amp;gt; artifacts with timestamp tags matching &amp;lt;code&amp;gt;^[0-9]{14}$&amp;lt;/code&amp;gt;, deletes older artifacts by digest, and sweeps any already-untagged artifacts left behind by previous cleanup runs. This is housekeeping, not heroism, but housekeeping is how registries avoid becoming museums of forgotten builds.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;opentofu-the-other-repo&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;OpenTofu: the other repo&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#opentofu-the-other-repo&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: OpenTofu: the other repo&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The OpenTofu workspace in &amp;lt;code&amp;gt;/Users/ellie/talos/opentofu-apps/augustini-wtf&amp;lt;/code&amp;gt; is small, which is exactly what you want for an app layer.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;It uses OpenTofu &amp;lt;code&amp;gt;&amp;amp;gt;= 1.12.0, &amp;amp;lt; 1.13.0&amp;lt;/code&amp;gt; and the HashiCorp Kubernetes provider &amp;lt;code&amp;gt;~&amp;amp;gt; 3.1.0&amp;lt;/code&amp;gt;. The lock file pins provider checksums. The provider config supports both local validation and Terrakube execution:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;provider&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;kubernetes&amp;amp;#34;&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; config_path&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; var.kubeconfig_path !&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;?&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;var&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;kubeconfig_path&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;:&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;null&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Locally, set &amp;lt;code&amp;gt;kubeconfig_path&amp;lt;/code&amp;gt;. In Terrakube, leave it empty and let the runner pod authenticate in-cluster through its ServiceAccount.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;State is not local, and Terrakube is not pretending to be the state backend. The backend is S3-compatible storage served by Rook Ceph RGW inside the cluster:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;terraform&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;backend&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;s3&amp;amp;#34;&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; bucket&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;tofu-state&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; key&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;augustini-wtf/terraform.tfstate&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; region&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;us-east-1&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; endpoints&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; s3&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;http://rook-ceph-rgw-ceph-objectstore.rook-ceph.svc:80&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;11&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; use_path_style&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;12&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; skip_credentials_validation&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;13&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; skip_region_validation&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;14&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; skip_metadata_api_check&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;15&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; skip_requesting_account_id&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;16&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;17&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That tells you quite a lot about the platform around the app: this is a Kubernetes-native setup, running on Talos, with Terrakube runners inside the cluster and Rook Ceph providing object storage for state. The app workspace does not define Talos itself, Rook itself, Terrakube itself, or the public Gateway. It attaches to those platform primitives.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That separation is healthy. App repo says &amp;amp;ldquo;run this site.&amp;amp;rdquo; Platform repos say &amp;amp;ldquo;here is the cluster, object storage, runner identity, gateway, DNS automation, and so on.&amp;amp;rdquo;&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;variables-small-escape-hatches-tight-defaults&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Variables: small escape hatches, tight defaults&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#variables-small-escape-hatches-tight-defaults&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Variables: small escape hatches, tight defaults&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The OpenTofu variables are boring in the pleasing sense.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The resource base name and namespace default to &amp;lt;code&amp;gt;augustini-wtf&amp;lt;/code&amp;gt;. The image repository defaults to &amp;lt;code&amp;gt;registry.augustini.xyz/sites/augustiniwtf&amp;lt;/code&amp;gt;. The image tag has a validation rule:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;variable&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;image_tag&amp;amp;#34;&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; description&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;Pinned container image tag.&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; type&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;string&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; default&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;20260607072706&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;validation&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; condition&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; var.image_tag !&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;latest&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; error_message&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;image_tag must be pinned to a concrete tag instead of latest.&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That single validation block prevents the classic personal-site incident where &amp;lt;code&amp;gt;latest&amp;lt;/code&amp;gt; works until the registry, node cache, rollout controller, and your assumptions all form a small committee and vote against you.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The Harbor credentials are sensitive variables. The registry host defaults to &amp;lt;code&amp;gt;registry.augustini.xyz&amp;lt;/code&amp;gt;. Replica count defaults to &amp;lt;code&amp;gt;2&amp;lt;/code&amp;gt;, with validation requiring at least one. CPU and memory defaults are intentionally tiny:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;requests&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; cpu&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;10m&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; memory&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;32Mi&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;limits&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; cpu&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;250m&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; memory&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;128Mi&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;This is a static web server. If it needs more than that under normal personal-site traffic, something is either famous or on fire.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;kubernetes-the-runtime-contract&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Kubernetes: the runtime contract&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#kubernetes-the-runtime-contract&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Kubernetes: the runtime contract&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The OpenTofu module creates a namespace with common labels:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;app.kubernetes.io/name&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;var&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;name&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;app.kubernetes.io/part-of&amp;amp;#34; = &amp;amp;#34;talos&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;app.kubernetes.io/managed-by&amp;amp;#34; = &amp;amp;#34;terrakube&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Then it merges in Pod Security Admission labels:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;pod-security.kubernetes.io/enforce&amp;amp;#34; = &amp;amp;#34;restricted&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;pod-security.kubernetes.io/audit&amp;amp;#34; = &amp;amp;#34;restricted&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;pod-security.kubernetes.io/warn&amp;amp;#34; = &amp;amp;#34;restricted&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That means the namespace is not merely &amp;amp;ldquo;please be secure&amp;amp;rdquo; flavored. It asks Kubernetes to enforce the restricted profile. The pod spec then matches that intent:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;automount_service_account_token&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;false&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;security_context&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; run_as_non_root&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; run_as_user&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;m&amp;#34;&amp;gt;10001&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; run_as_group&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;m&amp;#34;&amp;gt;10001&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; fs_group&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;m&amp;#34;&amp;gt;10001&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;seccomp_profile&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; type&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;RuntimeDefault&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;11&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;12&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The container security context tightens the rest:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;allow_privilege_escalation&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;false&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;read_only_root_filesystem&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;run_as_non_root&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;kt&amp;#34;&amp;gt;true&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;capabilities&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; drop&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;ALL&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;This lines up nicely with the scratch image. The image has nothing interesting to mutate, and Kubernetes makes the root filesystem read-only anyway. The pod gets no service account token by default. Linux capabilities are gone. Privilege escalation is off. Seccomp uses the runtime default.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;It is a very small box with a very small job.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The Deployment exposes one named container port:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;port&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; name&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;http&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; container_port&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;m&amp;#34;&amp;gt;8080&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; protocol&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;TCP&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Readiness and liveness probes both hit &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; through that named port. The Service maps port &amp;lt;code&amp;gt;80&amp;lt;/code&amp;gt; to target port &amp;lt;code&amp;gt;http&amp;lt;/code&amp;gt;, giving the Gateway a stable backend target without making the container pretend it is allowed to bind privileged ports.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;registry-auth-dockerconfigjson-as-infrastructure&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Registry auth: dockerconfigjson as infrastructure&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#registry-auth-dockerconfigjson-as-infrastructure&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Registry auth: dockerconfigjson as infrastructure&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Because the image lives in private Harbor, the module creates a &amp;lt;code&amp;gt;kubernetes.io/dockerconfigjson&amp;lt;/code&amp;gt; secret:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;data&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; &amp;amp;#34;.dockerconfigjson&amp;amp;#34;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;jsonencode&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;(&amp;lt;/span&amp;gt;{&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; auths&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; (var.harbor_url)&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; username&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;var&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;harbor_username&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; password&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;var&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;harbor_password&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; auth&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;base64encode&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;(&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;${var.harbor_username}:${var.harbor_password}&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;)&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;)&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;11&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;}&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;Then the Deployment references it via &amp;lt;code&amp;gt;image_pull_secrets&amp;lt;/code&amp;gt;.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is a good example of infrastructure code being allowed to be unglamorous. The cluster needs credentials to pull the image. Terrakube owns the sensitive inputs. OpenTofu renders the exact Kubernetes secret. The pod uses it. Nobody needs to click around in a registry UI and hope the namespace has the right secret forever.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;gateway-api-routing-as-an-app-owned-resource&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Gateway API: routing as an app-owned resource&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#gateway-api-routing-as-an-app-owned-resource&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Gateway API: routing as an app-owned resource&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The app does not define a load balancer. It attaches routes to an existing Gateway named &amp;lt;code&amp;gt;main-gateway&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;default&amp;lt;/code&amp;gt; namespace.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;For the apex hostname, the route attaches to section &amp;lt;code&amp;gt;https-wtf-apex&amp;lt;/code&amp;gt; and sends &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; to the Service:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;hostnames&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;var&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;hostname&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;parentRefs&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; group&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;gateway.networking.k8s.io&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; kind&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;Gateway&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; name&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;var&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;gateway_name&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; namespace&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;var&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;gateway_namespace&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; sectionName&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;var&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;.&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;gateway_https_section&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;11&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;The route also sets HSTS:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt;filters&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 2&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 3&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; type&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;ResponseHeaderModifier&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 4&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; responseHeaderModifier&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 5&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; set&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;[&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 6&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; {&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 7&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; name&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;Strict-Transport-Security&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 8&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; value&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;n&amp;#34;&amp;gt; &amp;amp;#34;max-age&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;o&amp;#34;&amp;gt;=&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;m&amp;#34;&amp;gt;63072000&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;includeSubDomains&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;;&amp;lt;/span&amp;gt; &amp;lt;span class=&amp;#34;k&amp;#34;&amp;gt;preload&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;err&amp;#34;&amp;gt;&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt; 9&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;10&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;11&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;12&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt; }&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;13&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;p&amp;#34;&amp;gt;]&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;There is a separate &amp;lt;code&amp;gt;www&amp;lt;/code&amp;gt; route attached to &amp;lt;code&amp;gt;https-wtf&amp;lt;/code&amp;gt; that returns a &amp;lt;code&amp;gt;301&amp;lt;/code&amp;gt; redirect to the apex host. There is also an optional HTTP route, enabled by default, that attaches to the Gateway&amp;amp;rsquo;s &amp;lt;code&amp;gt;http&amp;lt;/code&amp;gt; section and redirects both &amp;lt;code&amp;gt;augustini.wtf&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;www.augustini.wtf&amp;lt;/code&amp;gt; to HTTPS on the apex hostname.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;This is where Gateway API feels cleaner than the old Ingress pile. The platform owns the Gateway and listeners. The app owns its hostnames, redirects, headers, and backend references. The contract between them is explicit: route attaches to named listener section, listener accepts it, traffic moves.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;ExternalDNS is controlled through route annotations. If publishing is enabled, the route gets:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-hcl&amp;#34; data-lang=&amp;#34;hcl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;s2&amp;#34;&amp;gt;&amp;amp;#34;external-dns.alpha.kubernetes.io/include&amp;amp;#34; = &amp;amp;#34;true&amp;amp;#34;&amp;lt;/span&amp;gt;&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;If disabled, it gets a controller annotation pointing at an ignore value. That gives the app workspace a switch for &amp;amp;ldquo;publish records from these routes&amp;amp;rdquo; without making DNS a manual side quest.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;the-talos-part-and-what-this-repo-does-not-own&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;The Talos part, and what this repo does not own&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#the-talos-part-and-what-this-repo-does-not-own&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: The Talos part, and what this repo does not own&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The OpenTofu labels say &amp;lt;code&amp;gt;part-of = talos&amp;lt;/code&amp;gt;, and the path says the same thing: this app belongs to a Talos-based cluster estate. Talos matters here less because of any one line in the app module and more because of the operational model it implies.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Talos is not a general-purpose Linux host that also happens to run Kubernetes. It is an immutable Kubernetes appliance OS. No SSH as the normal management interface. No &amp;amp;ldquo;just apt install htop on the node.&amp;amp;rdquo; No artisanal node drift. Cluster operations go through APIs and declared configuration.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That philosophy matches the site deployment:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ul&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;The app container is scratch.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;The pod has a read-only root filesystem.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;The namespace enforces restricted Pod Security.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;The deployment is driven by OpenTofu from Terrakube.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;State lives in object storage, not on a laptop.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Routes attach to a pre-existing platform Gateway.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ul&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The website is tiny, but the operational shape is the same one you want for larger services: artifacts are built once, deployed by tag, reconciled by controllers, and exposed through platform-owned ingress.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;what-actually-changes-when-i-publish-a-post&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;What actually changes when I publish a post&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#what-actually-changes-when-i-publish-a-post&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: What actually changes when I publish a post&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;When this post gets merged and pushed, the pipeline does not mutate the cluster directly. It creates a new immutable-ish artifact and moves one pointer.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The only deployment variable Woodpecker changes is &amp;lt;code&amp;gt;image_tag&amp;lt;/code&amp;gt;. That variable goes from one timestamp to another. Terrakube runs the plan and apply. Kubernetes sees the Deployment pod template image change from:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;registry.augustini.xyz/sites/augustiniwtf:20260607072706&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;to something like:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-text&amp;#34; data-lang=&amp;#34;text&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;line&amp;#34;&amp;gt;&amp;lt;span class=&amp;#34;ln&amp;#34;&amp;gt;1&amp;lt;/span&amp;gt;&amp;lt;span class=&amp;#34;cl&amp;#34;&amp;gt;registry.augustini.xyz/sites/augustiniwtf:20260607123456&amp;#xA;&amp;lt;/span&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;p&amp;gt;That pod template change triggers a rollout. New pods pull the new image using the Harbor secret. Readiness probes pass. The Service selector keeps pointing at the same labels. The Gateway route keeps pointing at the same Service. Traffic gradually finds the new pods.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;There is no &amp;amp;ldquo;deploy content&amp;amp;rdquo; step. The content is inside the image. That makes rollback boring too: set &amp;lt;code&amp;gt;image_tag&amp;lt;/code&amp;gt; back to an older timestamp, apply, and let Kubernetes roll back to pods serving that older &amp;lt;code&amp;gt;public/&amp;lt;/code&amp;gt; tree.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;h2 id=&amp;#34;why-this-is-funny-and-why-it-is-not&amp;#34; class=&amp;#34;heading-with-permalink&amp;#34;&amp;gt;Why this is funny and why it is not&amp;lt;a&amp;#xA; class=&amp;#34;heading-permalink&amp;#34;&amp;#xA; href=&amp;#34;#why-this-is-funny-and-why-it-is-not&amp;#34;&amp;#xA; aria-label=&amp;#34;Copy link to section: Why this is funny and why it is not&amp;#34;&amp;#xA; data-heading-permalink&amp;#xA; &amp;gt;&amp;lt;span aria-hidden=&amp;#34;true&amp;#34;&amp;gt;#&amp;lt;/span&amp;gt;&amp;lt;/a&amp;gt;&amp;#xA;&amp;lt;/h2&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;Yes, this is a lot of machinery for personal fieldnotes with a heart icon in the header.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;But the machinery is not there because the website needs compute. It is there because I want the deployment path to teach the right habits:&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;ul&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Build static output deterministically.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Package the runtime as a small container.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Push to a registry.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Deploy pinned tags.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Keep infrastructure state remote.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Let CI request deployment, not impersonate the cluster admin.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Run with fewer privileges than the app could ever need.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;li&amp;gt;Put routing, redirects, and headers in declarative resources.&amp;lt;/li&amp;gt;&amp;#xA;&amp;lt;/ul&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The actual app is almost aggressively simple: HTML, CSS, a tiny amount of JavaScript, and a Rust static file server. The platform around it is the part that makes the simplicity durable.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;That is the nice thing about static sites. You can make the runtime so small that all the interesting engineering moves to the edges: how assets are built, how images are tagged, who is allowed to apply infrastructure, where state lives, how traffic enters, and what the pod is forbidden from doing.&amp;lt;/p&amp;gt;&amp;#xA;&amp;lt;p&amp;gt;The result is a terminal princess website that deploys like a grown-up service and runs like a locked filing cabinet with a web server taped to it. It is pastel. It is strict. It contains almost no moving parts. It sparks joy, and then immediately drops all Linux capabilities.&amp;lt;/p&amp;gt;&amp;#xA;</content:encoded></item></channel></rss>