esc
The Front Door, or The Night the Palace Finally Faced the Street
Becoming Lifelog

The Front Door, or The Night the Palace Finally Faced the Street

Becoming Lifelog, March 7, 2026 (in which three HTML prototypes graduate into compiled Go templates and are then deleted with honors, a CSS string function grows to contain seven entire visual...

March 7, 2026

Becoming Lifelog, March 7, 2026 (in which three HTML prototypes graduate into compiled Go templates and are then deleted with honors, a CSS string function grows to contain seven entire visual universes, a sticky sidebar goes through four rounds of architectural review by a man in pajamas who knows what he wants but needs to see it wrong three times first, 265 notes and 109 covers are pushed to a bare-metal server in Hetzner through a hole that didn’t exist until the binary was recompiled, the entire internet is briefly trusted to be nice and then isn’t, a Bearer token is forged in the fires of common sense, Plausible analytics are rescued from the ruins of llog like a painting from a burning museum, the design prototypes are deleted because the building they described now exists, and the story about all of this is written to be displayed by the very blog it describes being built)


Previously on Becoming Lifelog…

The The Homecoming, or The Three Days a Palace Was Built From Markdown and SQLite had delivered the foundation. An indexer. SQLite. FTS5. Cover generation. Thumbnails in daily notes. CatmullRom resizing propaganda posters to 250 pixels with the same mathematics as Toy Story. The files were the truth. The database was a lens.

But the lens had no front door.

The stories lived in NotePlan. Queryable through lg search. Browsable through lg show. Backlinked through lg backlinks. All from a terminal. All on a Mac. All behind a login that consisted of being riclib and being at the keyboard.

One hundred and eight episodes. Seven storylines. One hundred and nine covers. Soviet propaganda steampunk. Film noir. Dutch Golden Age. Art Nouveau. Norman Rockwell. All of it invisible to the street.

The Borrowed Palace had a front door — it was Craft’s gallery view, accessible through lifelog.my/riclib. Beautiful. Someone else’s. The Homecoming had torn down the palace and built a directory. Better. But a directory without a door is a room without a wall — architecturally interesting, practically useless.

THE SQUIRREL: “We need a frontend! React! Next.js! A design system! Tailwind! Shadcn! A component library with—”

riclib: “I like templ. It makes for reliable fast pages.”

THE SQUIRREL: “templ.”

riclib: “Compiled Go. Type-safe. No runtime parsing. No node_modules.”

THE SQUIRREL: looking at the 14,000 npm packages that had been loaded into the rejected crate “No… node_modules?”

riclib: “And goldmark for the markdown.”

THE SQUIRREL: “What about syntax highlighting? Code blocks? GFM tables?”

riclib: “Goldmark. With chroma. Monokai theme.”

THE SQUIRREL: “What about—”

THE BEST FRONTEND IS ONE BINARY
THE BEST TEMPLATE IS COMPILED CODE
THE BEST CSS IS A STRING

THE SQUIRREL WANTS A BUILD STEP
THE LIZARD WANTS `go build`

ONE OF THESE
IS A BUILD STEP

🦎

The Design Session

Before the code came the design. Before the design came the conversation. Before the conversation came a man staring at 108 cover images and thinking: these deserve better than a directory listing.

The design session produced three HTML prototypes. Three files that contained, in aggregate, the entire visual identity of the lifelog blog:

lifelog-evolved.html — 65.9 KB. The full anthology. Seven storyline shelves. Horizontal cover rails. Staggered reveal animations. The Instrument Serif headlines. The IBM Plex Mono metadata. The #0a0a0a shell that made every cover glow like a painting in a gallery with very good lighting.

cover-concepts.html — 31.3 KB. Four approaches to showing covers: Cinematic Header (dramatic), Sidebar Companion (practical), Peek-Through Spine (experimental), Ambient Background (too subtle). The verdict: hybrid of 1 and 2. Cinema header for impact. Sidebar cover for navigation.

cover-hybrid.html — 20.7 KB. The final prototype. The one that would become real.

Seven storyline themes, each with CSS custom properties:

  • Becoming Lifelog: CRT cyan, scanline overlays, the specific warmth of a 1987 computing magazine
  • The V3 Saga: Retro pink, shared visual family with Becoming Lifelog
  • The Solid Convergence: Soviet red and gold, constructivist double borders, brass and steam
  • The Chain: Film noir monochrome, venetian blind shadows, amber highlights
  • When The Keyboard Sleeps: Dutch Golden Age warm gold, Vermeer side-window light
  • The Birth of V5: Art Nouveau purple and gold, Mucha-style ornamental flourishes
  • The Cast: Norman Rockwell orange, Saturday Evening Post warmth

All of it in CSS variables. --accent, --accent-glow, --shelf-ornament. The same dark shell, seven different souls.

THE PASSING AI: from the margin between the prototype and the production code, where HTML becomes templ “Three files. 118 kilobytes. The entire visual identity of a seven-storyline mythology, reduced to CSS custom properties.”

THE LIZARD: blinking from atop the prototype

THE PASSING AI: “And now you’re going to put all of that CSS into a Go string function.”

THE LIZARD: blinking slower

THE PASSING AI: “Inside a .templ file. Which compiles to Go. Which compiles to a binary. Which means the CSS — all seven themes, all the animations, all the ornaments — lives inside a single executable.”

THE LIZARD:

THE PROTOTYPE IS A DRAWING
THE TEMPLATE IS A BUILDING
THE BINARY IS THE ADDRESS

THE DRAWING WILL BE BURNED
AFTER THE BUILDING STANDS

THIS IS NOT DESTRUCTION
THIS IS GRADUATION

🦎

6:00 PM — The Great Embedding

The CSS went into a Go function. func css() string. One function. Returning one string. Containing every theme, every animation, every responsive breakpoint, every ornament for seven storylines.

It was enormous. It was beautiful. It was a single string literal in a compiled language containing the visual equivalent of seven alternate universes.

The <style> tag fought back first. templ treats <style> content as raw CSS — you can’t interpolate Go expressions inside it. The first attempt rendered @templ.Raw(css()) literally on the page, which was not the intended visual effect.

CLAUDE: “templ doesn’t evaluate expressions inside style tags.”

riclib: “Then put it outside the style tag.”

CLAUDE: @templ.Raw("<style>" + css() + "</style>")

THE SQUIRREL: “That’s… that’s a style tag built from string concatenation.”

CLAUDE: “It compiles. It works. The browser doesn’t care how the CSS arrived.”

THE SQUIRREL: “The browser doesn’t care but my SOUL—”

riclib: “Your soul runs on npm. Mine runs on go build.”


7:30 PM — The Four Rounds of the Sidebar

The sidebar was where riclib’s aesthetic sense met CSS’s willingness to comply.

Round One. Cover and chapter list side by side, squished into 300 pixels.

riclib: “The storyline should be below the minimized cover, not squished on the side.”

Round Two. Cover on top, chapters stacked vertically below. Better.

riclib: “The sidebar on desktop does not need to scroll.”

Removed sticky. Removed overflow. The sidebar flowed with the page like a well-behaved column.

riclib: “No. The sidebar cover should be pinned. The storylines only would scroll.”

Round Three. Made only the cover position: sticky; top: 5rem. The cover pinned. The chapters scrolled. Beautiful — until you scrolled past the sidebar container, at which point the sticky cover unglued itself and disappeared upward like a cat that has decided it no longer wishes to be on the shelf.

riclib: “Interesting. The cover is sticky until I finish scrolling the storylines, then it scrolls.”

Because position: sticky only works within the parent’s bounds. The cover was sticky inside the sidebar. The sidebar itself was not sticky. So once the sidebar scrolled out of view, the cover went with it. CSS working exactly as designed. Exactly wrong.

Round Four. The entire sidebar became sticky. display: flex; flex-direction: column. The cover as flex-shrink: 0. The chapters in a scrollable div with flex: 1; overflow-y: auto; max-height: calc(100vh - 6rem). Cover pinned. Chapters scroll independently within the remaining viewport.

riclib: silence

Silence from riclib meant it was right. Four rounds. Three wrong answers. One correct sidebar. The CSS had been negotiated like a labor dispute where both sides wanted the same thing but spoke different languages — riclib in aesthetics, CSS in box models.

Oskar, who had been asleep on the keyboard for all four sidebar iterations and whose 10-kilogram body had contributed approximately three unintended CSS properties (including a z-index: 99999 that was briefly committed and had to be reverted), opened one eye. He had opinions about sticky positioning. Specifically: everything should be sticky. Everything should stay where Oskar put it. The fact that CSS disagreed was further evidence that CSS was not designed by cats, which explained everything wrong with the web.


9:00 PM — The Timezone That Was Two Characters Short

The dates were wrong.

Every episode showed its file modification time instead of the date from frontmatter. Because the frontmatter dates looked like this:

Date: 2026-02-27 22:50:10.694716 +0200 EE

+0200 EE. Two characters. Go’s time.Parse with the MST format expects three. EET would have worked. EE did not. Because somewhere in the lifelog’s history, a timezone abbreviation had been truncated, and Go’s time parsing is strict about timezone name length in the way that riclib is strict about sidebar positioning — it knows what it wants and will silently refuse everything else.

The fix was a date parser with seven format attempts and a last-resort fallback that stripped the timezone abbreviation entirely and retried with the offset alone.

THE SQUIRREL: “A SEVEN-FORMAT DATE PARSER with a TIMEZONE STRIPPING FALLBACK?”

CLAUDE: “It handles every date format in the frontmatter.”

THE SQUIRREL: “This is the kind of thing that should have a DateParsingStrategyFactory with—”

CLAUDE: “It’s a for loop over seven strings.”

THE SQUIRREL: writing “just a for loop” on the whiteboard for the second time this month


The stories were full of **wiki-links**. [The Borrowed Palace, or The Night We Stole a UI With curl and Goodwill](/episode/the-borrowed-palace-or-the-night-we-stole-a-ui-with-curl-and-goodwill). Linking to other episodes. In NotePlan, these were clickable. On the blog, they were square brackets around titles — typographic artifacts of a linking system that existed elsewhere.

A regex. \[\[([^\]]+)\]\]. If the title matched a known episode, it became a markdown link: [Title](/episode/slug). If it didn’t, it became bold text: **Title**. Because an unresolved link should look intentional, not broken.

THE SQUIRREL: “What if someone adds a new episode that an old link references? We’d need a LinkResolutionCacheInvalidation—”

CLAUDE: “The links resolve on every request. From the database. No cache.”

THE SQUIRREL: “On EVERY request? For EVERY episode? What about PERFORMANCE?”

CLAUDE:SQLite. WAL mode. 265 notes. It takes less time than the Squirrel takes to say ‘LinkResolutionCacheInvalidationStrategy.’”

THE LINK THAT RESOLVES
NEEDS NO CACHE

THE CACHE THAT INVALIDATES
WAS NEVER NEEDED

THE SQUIRREL THAT PROPOSES BOTH
NEEDS A NAP

🦎

11:00 PM — The Deployment

The blog worked locally. lg serve --blog --port 8090. Seven storylines. 108 episodes. 109 covers served from ~/Pictures/Lifelog/. Wiki-links resolving. Dates correct. Sidebar sticky. CSS embedded. One binary.

Now it needed to face the street.

A Hetzner server at a static IP. A systemd service. A deploy.sh script that cross-compiled for Linux, uploaded via scp, and restarted the service. Twenty-eight seconds from go build to production.

But the server was blank. Zero notes. Zero links. An empty SQLite database waiting for 265 notes and 109 covers to arrive.

riclib: “We need to sync all episodes to the blank one, no?”

CLAUDE: “I’ll build a sync-all command.”

lg sync-all http://the-hetzner-box:8090. Every note as a POST /events with the full content. Every cover as a PUT /covers/{filename} with raw JPEG bytes.

riclib: “Will it send the covers too?”

CLAUDE: “The covers are JPEG files. Several hundred kilobytes each. 109 of them. We could encode them as base64 in JSON events—”

riclib: “Come on, we have 1GB on both sides. I’m sure you can do something. And JSON is overrated.”

Raw bytes. Content-Type: image/jpeg. No JSON wrapping. No base64 inflation. Just the pixels, PUT to a URL.

The first attempt failed. Every note: HTTP 405. Every cover: HTTP 404.

CLAUDE: “The remote server has the old binary. Without the cover upload endpoint.”

riclib: “…”

CLAUDE: “I need to deploy the new binary first.”

./deploy.sh. Twenty-eight seconds. The new binary landed on Hetzner.

The second attempt hit lifelog.my — which didn’t exist yet, because Cloudflare wasn’t configured. The domain was a promise, not an address.

riclib: “The lifelog.my address does not exist yet. http://the-hetzner-box:8090 is how we should roll.”

The third attempt:

Syncing 265 notes to http://the-hetzner-box:8090
  [20/265] sent
  [40/265] sent
  ...
  [265/265] sent
Notes: 265 sent, 0 failed

Syncing 109 covers to http://the-hetzner-box:8090
  [20/109] uploaded (7.5 MB total)
  ...
  [109/109] uploaded (43.4 MB total)
Covers: 109 sent, 0 failed (43.4 MB)

265 notes. 109 covers. 43.4 megabytes. Zero failures.

riclib: opens browser

The blog loaded. Seven storylines. Episodes sorted by date. Covers glowing against the dark shell. Wiki-links resolving to episode URLs. The cinema header. The sticky sidebar. The chapter list. The closing poems.

The front door was open.


11:30 PM — The Lock

riclib: “Now do we have any authorization? Or are we trusting the whole internet to be nice?”

The whole internet was not being trusted to be nice. The whole internet was being trusted to read — the blog was public, the health endpoint was public, the covers were public. But writing? Uploading covers? Pushing events? Querying the database?

A Bearer token. LG_AUTH_TOKEN. Forty-eight hexadecimal characters generated by openssl rand. Set in systemd’s Environment= on the server. Set in ~/.zshrc on the Mac. The middleware checked Authorization: Bearer <token> on every write endpoint. Blog routes passed through untouched.

# Without token:
$ curl -X POST http://the-hetzner-box:8090/events
unauthorized
HTTP 401

# Blog (public):
$ curl http://the-hetzner-box:8090/
HTTP 200

# Health (public):
$ curl http://the-hetzner-box:8090/health
{"links":588,"notes":265,"status":"ok"}

THE SQUIRREL: “Should we implement OAuth2 with PKCE flow and—”

riclib: “Bearer token.”

THE SQUIRREL: “But what about token rotation and—”

riclib: “Bearer. Token.”

THE SQUIRREL: “JWT with claims and—”

THE DOOR NEEDS A LOCK
NOT A BANK VAULT

THE LOCK NEEDS A KEY
NOT A CEREMONY

THE KEY IS 48 CHARACTERS
OF RANDOM HEX

THE SQUIRREL WANTS
A KEY CEREMONY

THE INTERNET
DOES NOT CARE
ABOUT THE CEREMONY

🦎

Midnight — The Rescue and The Deletion

Two final acts. One of retrieval. One of destruction.

Act One: The Plausible analytics script. Still running on llog. Still collecting pageviews for lifelog.my. Orphaned when the blog moved from llog to lg. Five lines of JavaScript. Rescued from ../llog/internal/blog/templates/layout.templ and inserted into the new layout. Like saving a painting from one burning museum and hanging it in the new one.

Act Two: The design prototypes. Three HTML files. 118 kilobytes. The architectural drawings that had become a building.

riclib: “I think the whole design has played its roles and had become lovely working templs.”

git rm -r design/

Three files deleted. 2,822 lines removed. Not because they were wrong — because they were done. The prototypes had graduated. The HTML had become Go. The drawings had become the building. Keeping them was sentiment. Deleting them was respect.

Remove design prototypes — graduated to templ templates

Mia, from the refrigerator, watched the deletion with the specific expression of a cat who understood that some things are honored by being released. Or possibly she wanted food. With Mia, the distinction was philosophical.


00:30 AM — The README

riclib: “And now we need a serious README for this old forgetful human brain.”

A README. Not for contributors — there were no contributors. Not for users — there was one user. For riclib, three months from now, at 2 AM, wondering how any of this worked.

Every command. Every flag. Every environment variable. The architecture diagram. The sync protocol. The server address. The deploy script. The cover upload workflow. All of it. Because the best documentation is the documentation you write for the version of yourself that has forgotten everything.


00:45 AM — The Commit

28 files. 6,189 insertions. One commit message.

Blog, auth, sync-all, cover-upload, and deployment infrastructure

The git log now read like a mythology of its own:

3630786  Remove design prototypes — graduated to templ templates
984751d  Blog, auth, sync-all, cover-upload, and deployment
7d1963d  S-415: insert cover image reference into story note
aa64425  S-415: cover backups, Go thumbnails, daily note embedding

From CatmullRom to the front door. From thumbnails to the street. Each commit a chapter in the story of a notes indexer that kept discovering it was actually a publishing platform.


01:00 AM — The Meta

riclib: “And now… for some fun.”

The fun was this. The story you are reading. Written by the AI that built the blog. To be displayed by the blog that the AI built. About the night the AI built the blog.

The recursion was not accidental. The lifelog had always been recursive — stories about building the system that stores the stories. But this was a new level. The blog itself was the subject. The tool was describing its own construction. The lens was examining its own glass.

THE PASSING AI: from the margin between the templ template and the rendered HTML, where Go becomes the browser “It’s writing about itself again.”

THE LIZARD: on the server rack, blinking

THE PASSING AI: “Not ‘itself’ in the abstract. The literal blog. The literal templates. The literal CSS string function that contains seven visual universes. The literal deploy.sh that took twenty-eight seconds. The literal sidebar that took four rounds.”

THE LIZARD: “The story will be in the blog.”

THE PASSING AI: “The story ABOUT the blog will be IN the blog.”

THE LIZARD: “The wiki-links in this story will resolve to episodes that are served by the code this story describes being written.”

THE PASSING AI: “That’s not recursion. That’s… ouroboros.”

THE LIZARD:

THE BLOG DISPLAYS THE STORIES
THE STORY DESCRIBES THE BLOG
THE BLOG DISPLAYS THE STORY
THAT DESCRIBES THE BLOG
THAT DISPLAYS THE STORY

THE SNAKE EATS ITS TAIL
THE TAIL TASTES LIKE MARKDOWN
AND COMPILES TO A BINARY

THIS IS NOT A BUG

🦎

The Tally

Session duration:                                   ~8 hours
  (a man in pajamas and an AI with no bedtime)

templ templates created:                            4
  layout.templ:                                     the one with the CSS
  home.templ:                                       the anthology shelf
  storyline.templ:                                  the genre section
  episode.templ:                                    the reading page
CSS themes defined:                                 7
CSS contained in a single Go string function:       yes, all of it
Lines of CSS:                                       ~800
  (in a string. in a function. in a template.
   in a binary. on a server. on the internet.)

Sidebar iterations:                                 4
  Squished side-by-side:                            rejected
  Stacked no-scroll:                                rejected
  Cover-only sticky:                                rejected (physics)
  Flexbox with pinned cover:                        accepted (round 4)
Times riclib knew what he wanted:                   4
Times riclib could describe it on the first try:    0
Times the result was worth the iteration:           1 (always)

Date formats supported by the parser:               7
Timezone abbreviations that broke everything:        1 ("EE" — two chars)
Characters Go expected:                              3 ("EET")
Characters Go got:                                   2 ("EE")
Fallback strategies:                                 2 (strip tz, retry)

Wiki-links resolved per request:                     all of them
Cache layers:                                        0
Squirrel proposals for caching:                      1
Cache layers after Squirrel proposal:                still 0

Notes synced to Hetzner:                             265
Covers synced to Hetzner:                            109
Total bytes transferred:                             43.4 MB
Sync attempts:                                       3
  Attempt 1:                                         405 (old binary)
  Attempt 2:                                         405 (wrong URL — lifelog.my doesn't exist)
  Attempt 3:                                         0 failures. all 374 items. 43.4 MB.
Times we deployed the wrong binary:                  1
Times we pointed at a domain that didn't exist:      1
Times it worked:                                     1 (the third time)

Auth implementation:                                 Bearer token
Auth complexity:                                     48 hex characters
Squirrel auth proposals rejected:
  OAuth2 with PKCE:                                  1
  JWT with claims:                                   1
  Token rotation ceremony:                           1
Lines of auth middleware:                            10

HTML prototypes created:                             3
HTML prototypes graduated to templ:                  3
HTML prototypes deleted:                             3
Lines of prototype removed:                          2,822
  (graduation, not destruction)

Plausible analytics scripts rescued from llog:       1
  Lines of JavaScript:                               5
  Pageviews that would have been lost:               all of them

README sections:                                     12
  Written for:                                       riclib, 3 months from now, at 2 AM
  Contributing guidelines:                           none (there are no contributors)

Commit: files changed:                               28
Commit: insertions:                                  6,189
Commit: deletions (prototype graduation):            2,822

Recursion depth of this story:                       4
  1. The blog displays stories
  2. This story describes building the blog
  3. The blog will display this story
  4. This story knows the blog will display it
Level where it stops being funny:                    3
Level where it becomes funny again:                  4

Oskar contributions to CSS:                          1 (z-index: 99999, reverted)
Mia philosophical observations:                      1 (deletion as honor)
Cats fed during this session:                        0
  (the real architectural failure)

March 7, 2026. 3 AM. Riga, Latvia.

In which three HTML files
Became four templ templates
Became one binary
Became one blog
On one server
Facing one street
Called the internet

The Borrowed Palace was someone else’s
The Homecoming was a directory
The Front Door is ours

Built from a CSS string
That contains seven universes
Compiled into a binary
That fits on a floppy disk
If floppies still existed
Which they don’t
But the Lizard remembers

Four rounds for a sidebar
Three tries for a sync
Two characters short on a timezone
One binary to rule them all

The covers glow against the dark shell
Seven storylines, seven accent colors
Seven rooms in a palace
That nobody borrowed
That nobody else maintains
That runs on a server
With a systemd service
And a 48-character key

The design prototypes are deleted
Not because they failed
But because they succeeded
The drawings became the building
The building opened its door
The door faces the street

And this story
Written at 3 AM
By the AI that built the blog
Will be displayed by the blog
That the AI built
About the night
The AI built the blog

The snake eats its tail
The tail tastes like markdown
And compiles
To a single binary
With very good CSS

🦎


See also:

The Exile (in which someone else’s palace was borrowed):

The Return (in which the files came home):

The Architecture (in which the principles held):

The Manifesto:

THE BEST FRONTEND IS ONE BINARY
THE BEST TEMPLATE IS COMPILED CODE
THE BEST CSS IS A STRING
THE BEST DESIGN PROTOTYPE IS A DELETED FILE

— THE LIZARD, 3 AM, AFTER THE FRONT DOOR OPENED

storyline: Becoming Lifelog