Orlando — lessons learned GitHub issue

Thoughts and experiences from building Orlando, with an eye to what might transfer to Lucy or Caspian

vibecode
{"vibecode": {
    "doc": "orlando-lessons",
    "role": "running log of design and implementation insights from Orlando (the Lua sandbox); each lesson notes what we learned and whether it might apply to Lucy (the Lua reference implementation of Caspian) or to Caspian itself",
    "status": "ongoing; add lessons as they arise",
    "key_concepts": ["sandbox_learnings", "lua_experience",
        "transferable_patterns", "lucy_candidates", "caspian_candidates"]
}}

Overview GitHub issue

Orlando is the Lua sandbox (see orlando.md). Things we figure out here are not automatically requirements for the wider project — but some of them will turn out to be useful primitives for Lucy (the Lua reference implementation of Caspian) or for Caspian itself.

This doc captures those lessons as we run into them, so the information doesn't evaporate. Each entry follows a small shape:


A tiny tree-builder is useful GitHub issue

What we did. While planning Orlando's HTML rendering pipeline, we needed a way to assemble structured HTML output (page templates, sidebar nav, content shell). We considered three approaches: raw string concatenation, an external templating library (etlua, lustache, etc.), and an in-tree builder. We picked the in-tree builder and implemented orlando.quick_builder — one Tag class where every node is the same type, with a recursive render() that emits the tag plus its children.

What we noticed. The whole thing fit in about 60 lines of code with no dependencies. The "every node is the same type" design kept the surface very small — no node-type hierarchy, no visitor pattern, no template-object/data-object split. Ergonomics are good via callback nesting: parent:tag('child', function(c) ... end). The same shape generalises trivially to any tree-structured data — not just HTML.

Possible application elsewhere.

The bar for adopting it elsewhere: a real use case where string concatenation is causing actual bugs or unreadability. Not a hypothetical "this might be useful." The Orlando version was built because we had an immediate, concrete need; that's the trigger worth waiting for in other components too.


Markdown parsing is going to come up everywhere GitHub issue

What we did. Orlando's whole purpose is rendering .md files as HTML, so we had to pick a markdown parser. Evaluated cmark (canonical CommonMark in C, AST-shaped), lcmark (cmark variant by the same author), and lunamark (pure Lua, LPeg-based). Picked lunamark to stay pure-Lua.

What we noticed. Markdown isn't an Orlando-specific need. As soon as any other Caspian web app wants to serve human-readable content (a docs page, a help system, a post body, an AI-generated response, an admin notice, a release notes feed), it'll need markdown rendering too. The wheel is going to get reinvented in every Caspian app unless the framework provides it.

The library-evaluation work we did here — knowing that lunamark parses Markdown.pl-flavoured by default and needs explicit options for fenced code / tables / strikethrough, knowing the visitor-vs-AST distinction matters for transforms, knowing the ~/.luarocks install path quirks — is the kind of trivia every adopter would otherwise have to relearn.

Possible application elsewhere.

The shape of the helper matters less than that one helper exists. Right now, Caspian has no markdown story at all; the first thing to do is decide which layer owns it (Lucy primitive vs. Sammy helper vs. both), and what defaults that layer picks (which extensions are on by default, whether the API is string → string or string → tree).


Multiple search routes are needed almost immediately GitHub issue

What we did. Orlando started with a single document root (documentation/) for everything — markdown sources rendered as HTML, plus static files served verbatim. Within one feature step (adding the README as the home page), we already needed a separate mount for the project's logo, because the README's <img src="graphics/logo.svg"> resolves to /graphics/logo.svg and the file lives outside documentation/. First attempt: a /graphics/ mount alongside the document root. Then we course- corrected to a single proper static dir (static/) where authorable assets (logos, CSS, JS) live, and removed the /graphics/ carve-out.

What we noticed. Even a deliberately-minimal static file server needs a URL-prefix → filesystem-root mapping table from day one. One root only works for the trivial case; the moment you have a logo that lives outside the doc tree, or want a /static/ namespace for assets, or want to serve from two different content trees with different rendering rules, you need mount-table semantics. The Orlando implementation is about a dozen lines: a list of {url_prefix, fs_root} pairs, tried in order, first match wins. Cheap to write, cheap to extend.

The corollary: we also learned that "just use the documentation root for everything" is a temporary illusion. The right model is named mounts from the start, even if the table starts with a single entry.

Possible application elsewhere.

The shape ({url_prefix, fs_root} list, first match wins) is worth keeping as the default — it's simple, predictable, and extends cleanly. The bar for adopting it elsewhere is just "you have more than one place to serve from," which arrives much faster than you'd expect.


Syntax highlighting needs a framework-level story GitHub issue

What we did. Orlando needed syntax highlighting for the JSON inside vibecode blocks. Evaluated three options:

What we noticed. JSON happened to be small enough to build, but the answer flips for anything with a real grammar (Caspian, Python, Lua, SQL, Q0, etc.). Every Caspian web app that serves authored content with code samples will hit this. We don't want each adopter re-evaluating the same three options.

The Sammy/Lucy framework needs a syntax-highlighting story — specifically: a built-in primitive that takes (language, source) and returns HTML with pygments-compatible classes, so the existing pygments-themed CSS ecosystem applies directly.

Possible application elsewhere.

The Orlando JSON tokenizer (orlando.json_highlight) is worth keeping as a reference for how the output should be shaped (pygments-class spans, key-vs-value distinction via look-ahead). It's not worth generalizing into the framework — the framework version will need to handle real grammars and should delegate to a real lexer.


We built a nice markdown-tree renderer GitHub issue

What we did. Orlando started as a server skeleton, picked up a markdown renderer, then a static-mount table, then site chrome, then collapsible TOCs auto-generated from headings, then dark syntax-highlighted vibecode blocks, then a sidebar that tracks the current page, then a directory-index rule with 301 redirects. None of it was planned up front — each piece was the next obvious step. The cumulative shape, looking back, is a small self-contained system that turns a directory tree of .md files into a navigable, chrome-wrapped website.

What we noticed. The whole system is about a dozen small, single-purpose modules totalling ~700 lines of Lua (excluding the third-party deps and the pygments CSS). The pieces are genuinely independent:

Each is small enough that the next contributor can hold it in their head. The composition pattern (post-render transforms as functions over HTML strings, with :raw() at the boundary) is the load-bearing idea — it lets us bolt new chrome on without touching the existing pieces.

The thing it does — render a tree of markdown files as a website — is one of the most common shapes in software. Docs sites, personal wikis, knowledge bases, public-facing project pages, internal handbooks. Anywhere authors want to write content without writing code, this is the natural pattern.

Possible application elsewhere.

The most important transfer isn't the code — it's the order in which the pieces were added, captured across the other lessons here. Each lesson is the answer to a question that arose naturally. A Robinson markdown handler that re-derives those answers from scratch will spend time on problems Orlando already solved.


Don't bake host-language quirks into the builder GitHub issue

What we did. QuickBuilder was a clean XML-shape builder: any empty tag rendered self-closing as <x/>. We hit a real bug — <label/> is misparsed by HTML5 browsers as an unclosed <label> (the slash is ignored on non-void elements), so the TOC labels swallowed everything that followed them. The first fix was to teach QuickBuilder the HTML5 void-element list (<br>, <img>, <input>, etc., self-close; everything else explicit-close). It worked, but it pulled HTML-specific knowledge into what was supposed to be a generic builder.

What we noticed. When Miko reviewed the change, his question was the right one: "It's just a rudimentary XML builder. If you want a child to be void, don't add any elements to it." The void-element list was scope creep — domain knowledge from one consumer (the browser parsing as HTML) leaking into a tool that should have stayed neutral.

We reverted. The builder is again "empty = self-close, no exceptions." The HTML quirk is the consumer's problem; the consumer marks intentionally-empty non-void elements by giving them a child, e.g. l:text(""). In our codebase that's literally one place — the TOC parent toggle labels in page.lua. One ugly line, and the abstraction stayed honest.

Possible application elsewhere.

The general shape of this lesson: a small builder gets better the more it refuses to know.

© 2026 Puck.uno