Back to flin
flin

The Layout Children Wrapping Bug

When {children} in layouts wrapped content in unexpected HTML elements.

Thales & Claude | March 25, 2026 7 min flin
flinbuglayoutchildrenflinuiwrapping

Layouts are the skeleton of a web application. They provide the header, the footer, the navigation, the sidebar -- the structural elements that remain constant as the user navigates between pages. The page-specific content fills the gap in the middle. This pattern is so fundamental that every web framework implements some version of it.

FLIN's approach uses a {children} placeholder in layout files:

// layouts/default.flin
<div class="app">
    <header>Navigation here</header>
    <main>{children}</main>
    <footer>Footer here</footer>
</div>

Page files contain only their specific content, and the layout wraps around it. Simple in concept. But the implementation revealed a cascade of bugs in rendering, file watching, layout selection, and URL handling that took the full span of Session 250 to resolve.

The Initial Problem

The modern-notes application had a layout file and page files, but the layout was not being applied. Page content rendered without any header, footer, or navigation. The {children} placeholder in the layout was never replaced with page content.

The root cause was architectural: the rendering pipeline had no concept of layout wrapping. The server compiled each page file independently and sent the rendered HTML directly to the browser. The layout file existed but was never read, parsed, or applied.

Building the Layout Pipeline

The fix required changes across four layers of the system.

Layer 1: Layout Registry

The layout registry (src/layouts/registry.rs) needed to expose the view portion of layout files. We made extract_view_code() public and added a get_layout_view() method:

pub fn get_layout_view(&self, layout_name: &str) -> Option<String> {
    let layout_source = self.get_layout(layout_name)?;
    let view_code = extract_view_code(&layout_source)?;
    Some(view_code)
}

This extracts the HTML template from a layout file, returning the view portion that contains the {children} placeholder.

Layer 2: Renderer

The renderer (src/vm/renderer.rs) needed a new function to combine layout HTML with page HTML:

pub fn render_with_layout(
    page_html: &str,
    layout_view: &str,
    vm: &mut VM,
) -> String {
    // Parse the layout view as FLIN template
    let layout_ast = parse_view_only(layout_view);

// Render the layout, replacing {children} with page HTML render_layout_ast(&layout_ast, vm, page_html) } ```

The {children} placeholder is a special expression that the renderer recognizes during layout rendering. When it encounters {children}, it inserts the pre-rendered page HTML verbatim.

Layer 3: Library API

Two new public functions were added to src/lib.rs:

pub fn compile_and_render_with_layout(
    source: &str,
    layout_view: Option<&str>,
) -> Result<String, CompileError> {
    let page_html = compile_and_render(source)?;
    match layout_view {
        Some(view) => Ok(render_with_layout(&page_html, view, &mut vm)),
        None => Ok(page_html),
    }
}

Layer 4: HTTP Server

The server updated both serve_flin() and serve_flin_route() to look up the layout for each page and apply it during rendering.

The Multi-Layout Problem

Once basic layout wrapping worked, a new requirement emerged: different pages need different layouts. A login page should not have the full navigation header. An OTP verification page might need no layout at all.

We implemented a layout declaration syntax at the top of page files:

// Uses the auth layout
layout = "auth"
email = ""
<div>Login form here</div>

// Uses no layout layout = none

Full custom page

// No declaration: uses default layout

Normal page content
```

The server extracts the layout name before compilation:

fn extract_layout_name(source: &str) -> &str {
    // Parse first non-comment line for layout = "name"
    // Returns: "auth", "none", or "default"
}

This required careful parsing because the layout = "name" declaration must be the very first line of the file. If it appears after any code, it is too late -- the compiler would interpret it as a variable assignment.

The Hot Reload Gap

With layouts working, we discovered that changes to layout files did not trigger hot reload. Editing layouts/default.flin required a full server restart to take effect.

The file watcher in src/main.rs was only watching the app/ directory and styles/ directory. It had no concept of lib/ or layouts/ directories. We added detection for both:

// In the file watcher callback
if path_str.contains("/lib/") {
    lib_registry.write().reload_all();
    eprintln!("lib/ reloaded");
}

if path_str.contains("/layouts/") { layout_registry.write().reload_all(); eprintln!("layouts/ reloaded"); } ```

This required exposing the registry instances from the server through new getter methods:

pub fn lib_registry(&self) -> Arc<RwLock<LibRegistry>> {
    self.lib_registry.clone()
}

pub fn layout_registry(&self) -> Arc> { self.layout_registry.clone() } ```

The URL Rendering Bug

A minor but persistent bug surfaced in the layout's banner text. The text https://flin.dev displayed incorrectly because the :// sequence was being misinterpreted by the lexer:

// Caused rendering issues
<a href="https://flin.dev">https://flin.dev</a>

// Workaround: avoid :// in text content flin.dev ```

The lexer was treating :// as a token boundary, splitting the URL into separate text fragments. This was not worth fixing in the lexer for the v1.0 release -- the workaround of using short domain names in display text was sufficient.

The Shutdown Fix

While testing hot reload, we discovered that Ctrl+C did not terminate the server cleanly. The server caught the signal but lingering async tasks kept the process alive. The fix was pragmatic:

// In the signal handler
std::process::exit(0);  // Force terminate lingering async tasks

This is not the most elegant approach -- it does not give pending operations time to complete -- but for a development server, immediate shutdown on Ctrl+C is the expected behavior.

The Architecture After the Fix

The complete layout system works as follows:

Page Request
    |
    v
Extract layout name from source
    |
    +-- "none" -> Render page only, no layout
    |
    +-- "auth" -> Find layouts/auth.flin
    |
    +-- "default" (or no declaration) -> Find layouts/default.flin
    |
    v
Prepend layout state/functions to page source
    |
    v
Compile and render page
    |
    v
Replace {children} in layout view with page HTML
    |
    v
Send complete HTML to browser

The layout's state and functions (theme toggle, language switch, etc.) are prepended to the page source before compilation. This means page code has access to layout-defined functions and variables. The rendered page HTML is then inserted into the layout's view at the {children} position.

The Fragment Wrapping Pattern

One subtle issue emerged with page content: pages that contain multiple top-level elements need a fragment wrapper:

// Page with multiple top-level sections
<>
    <section class="hero">Hero content</section>
    <section class="content">Main content</section>
    <section class="sidebar">Side content</section>
</>

The <>... fragment syntax groups multiple elements without adding an extra DOM node. Without it, the renderer only captures the first top-level element as the page content.

Lessons Learned

The layout system implementation reinforced several principles.

Layout declaration must be positional, not semantic. The layout = "auth" declaration must be the first line because the server needs to know which layout to use before compiling the page. If the declaration were allowed anywhere in the file, the server would need to parse the entire file to find it -- which defeats the purpose of having it.

Hot reload must cover all content types. Developers expect changes to any file in the project to be reflected immediately. Watching only application files and stylesheets is insufficient; layouts, library files, translations, and configuration files all need watching.

The {children} pattern is deceptively simple. Replacing a placeholder with HTML seems trivial, but the implementation touches the lexer (parsing {children} as a special expression), the renderer (recognizing and expanding it), the server (determining which layout to use and orchestrating the pipeline), and the file watcher (detecting changes in layout files). A feature that is one concept to the developer is five changes to the framework.

The layout system went from non-existent to fully functional in a single session: basic wrapping, multi-layout support, hot reload, auth pages, and fragment wrapping. It was one of those sessions where the problem kept expanding as we solved each layer, but each expansion was necessary for the feature to be genuinely useful rather than merely functional.

---

This is Part 163 of the "How We Built FLIN" series, documenting how a CEO in Abidjan and an AI CTO designed and built a programming language from scratch.

Series Navigation: - [162] The Database Persistence Fix That Took 3 Sessions - [163] The Layout Children Wrapping Bug (you are here) - [164] Fixing Library Function Resolution

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles