Bläddra i källkod

refactor(demo): convert demo app to documentation site

Restructure the demo application into a comprehensive documentation site for the Spry framework:

- Replace landing page components with documentation-focused layout
- Add NavSidebarComponent for documentation navigation
- Create documentation pages for components, page components, and static resources
- Add interactive demo components (Counter, TodoList, Progress)
- Extend resource handling in Component.vala to support script, img, and link tags
- Add validation for spry-outlet elements requiring sid attribute
- Remove debug trace calls from PageComponent.vala
- Generate CSS resources via mkssr tool in meson build
Billy Barrow 6 dagar sedan
förälder
incheckning
6c0ced6f1b

+ 0 - 33
demo/Components/AuroraWaveComponent.vala

@@ -1,33 +0,0 @@
-using Astralis;
-using Inversion;
-using Spry;
-
-/**
- * AuroraWaveComponent - A single aurora wave strip
- * 
- * Renders as a CSS gradient strip with wave animation.
- * Multiple waves layered create the aurora borealis effect.
- */
-public class AuroraWaveComponent : Component {
-    
-    public int wave_id { get; set; }
-    public double y_offset { get; set; }
-    public double amplitude { get; set; }
-    public double frequency { get; set; }
-    public string color1 { get; set; default = "#22c55e"; }
-    public string color2 { get; set; default = "#7c3aed"; }
-    public double opacity { get; set; default = 0.6; }
-    public double animation_delay { get; set; default = 0; }
-    
-    public override string markup { get {
-        return """
-        <div sid="wave" class="aurora-wave"></div>
-        """;
-    }}
-    
-    public override async void prepare() throws Error {
-        // Build CSS custom properties as the style attribute
-        var style = @"--wave-y: $(y_offset)%%; --wave-amplitude: $(amplitude)px; --wave-freq: $(frequency); --wave-color1: $(color1); --wave-color2: $(color2); --wave-opacity: $(opacity); --wave-delay: $(animation_delay)s; animation-delay: var(--wave-delay);";
-        this["wave"].set_attribute("style", style);
-    }
-}

+ 105 - 0
demo/Components/DemoHostComponent.vala

@@ -0,0 +1,105 @@
+using Spry;
+
+/**
+ * DemoHostComponent - A component that hosts demo implementations with source/demo toggle
+ * 
+ * This component provides a frame for demo content with the ability to toggle
+ * between viewing the demo and viewing the source code.
+ * 
+ * Usage:
+ *   var host = factory.create<DemoHostComponent>();
+ *   host.source_file = "examples/CounterComponent.vala";
+ *   host.set_outlet_child("demo-outlet", demo_component);
+ */
+public class DemoHostComponent : Component {
+    
+    /// Path to the source file to display (relative to project root)
+    public string source_file { get; set; default = ""; }
+    
+    /// Toggle state - true when showing source code
+    public bool showing_source { get; set; default = false; }
+    
+    /// The loaded source code content (cached after first load)
+    private string? _source_content = null;
+    
+    public override string markup { get {
+        return """
+        <div class="demo-host" sid="host">
+            <div class="demo-header">
+                <span class="demo-title" sid="title">Demo</span>
+                <div class="demo-toggle-group">
+                    <button class="demo-toggle-btn" spry-action=":ShowDemo" spry-target="host" 
+                            class-active-expr="!this.showing_source">Demo</button>
+                    <button class="demo-toggle-btn" spry-action=":ShowSource" spry-target="host"
+                            class-active-expr="this.showing_source">Source</button>
+                </div>
+            </div>
+            <div class="demo-content" sid="content">
+                <div spry-if="!this.showing_source">
+                    <spry-outlet sid="demo-outlet"/>
+                </div>
+                <div spry-if="this.showing_source">
+                    <pre class="demo-source"><code sid="source-code"></code></pre>
+                </div>
+            </div>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Update the source code display if we're showing source
+        if (showing_source && _source_content != null) {
+            this["source-code"].text_content = _source_content;
+        }
+    }
+    
+    public async override void handle_action(string action) throws Error {
+        switch (action) {
+            case "ShowDemo":
+                showing_source = false;
+                break;
+            case "ShowSource":
+                if (_source_content == null) {
+                    _source_content = yield load_source_file();
+                }
+                showing_source = true;
+                break;
+        }
+    }
+    
+    /**
+     * Load the source file from disk and escape HTML entities
+     */
+    private async string load_source_file() {
+        try {
+            // Try to read from the project root (relative path)
+            var file = File.new_for_path(source_file);
+            
+            if (!file.query_exists()) {
+                return @"Error: Source file not found: $source_file";
+            }
+            
+            uint8[] contents;
+            string etag_out;
+            yield file.load_contents_async(null, out contents, out etag_out);
+            
+            var source = (string)contents;
+            return escape_html(source);
+        } catch (Error e) {
+            return @"Error loading source file: $(e.message)";
+        }
+    }
+    
+    /**
+     * Escape HTML entities for safe display in <pre><code> blocks
+     */
+    private string escape_html(string input) {
+        var result = input
+            .replace("&", "&amp;")
+            .replace("<", "&lt;")
+            .replace(">", "&gt;")
+            .replace("\"", "&quot;")
+            .replace("'", "&#39;");
+        return result;
+    }
+}

+ 0 - 31
demo/Components/FeatureCardComponent.vala

@@ -1,31 +0,0 @@
-using Spry;
-
-/**
- * FeatureCardComponent - A reusable feature card
- * 
- * Displays an icon, title, and description in a styled card
- */
-public class FeatureCardComponent : Component {
-    
-    public string icon { set; get; default = "purple"; }
-    public string icon_emoji { set; get; default = "⭐"; }
-    public string title { set; get; default = "Feature"; }
-    public string description { set; get; default = "Description"; }
-    
-    public override string markup { get {
-        return """
-        <div class="feature-card">
-            <div class="feature-icon" sid="icon"></div>
-            <h3 sid="title"></h3>
-            <p sid="description"></p>
-        </div>
-        """;
-    }}
-    
-    public override async void prepare() throws Error {
-        this["icon"].text_content = icon_emoji;
-        this["icon"].set_attribute("class", @"feature-icon $icon");
-        this["title"].text_content = title;
-        this["description"].text_content = description;
-    }
-}

+ 146 - 0
demo/Components/NavSidebarComponent.vala

@@ -0,0 +1,146 @@
+using Spry;
+
+/**
+ * NavSidebarComponent - A tree-based navigation sidebar for the documentation site
+ * 
+ * Features:
+ * - Collapsible sections for parent items
+ * - Current page highlighting
+ * - Automatic expansion of section containing current page
+ */
+public class NavSidebarComponent : Component {
+    
+    public string current_path { get; set; default = "/"; }
+    
+    // Track expanded state for each section
+    private bool _components_expanded = false;
+    private bool _page_components_expanded = false;
+    private bool _static_resources_expanded = false;
+    
+    public override string markup { get {
+        return """
+        <aside class="nav-sidebar" sid="sidebar">
+            <!-- Home - Single link -->
+            <div class="nav-section">
+                <a href="/" class="nav-item" sid="home-link">Home</a>
+            </div>
+            
+            <!-- Components Section -->
+            <div class="nav-section" sid="components-section">
+                <button class="nav-section-header" spry-action=":ToggleComponents" spry-target="components-items">
+                    Components
+                </button>
+                <ul class="nav-items" sid="components-items">
+                    <li><a href="/components/overview" class="nav-item" sid="components-overview">Overview</a></li>
+                    <li><a href="/components/template-syntax" class="nav-item" sid="components-template-syntax">Template Syntax</a></li>
+                    <li><a href="/components/actions" class="nav-item" sid="components-actions">Actions</a></li>
+                    <li><a href="/components/outlets" class="nav-item" sid="components-outlets">Outlets</a></li>
+                    <li><a href="/components/continuations" class="nav-item" sid="components-continuations">Continuations</a></li>
+                </ul>
+            </div>
+            
+            <!-- Page Components Section -->
+            <div class="nav-section" sid="page-components-section">
+                <button class="nav-section-header" spry-action=":TogglePageComponents" spry-target="page-components-items">
+                    Page Components
+                </button>
+                <ul class="nav-items" sid="page-components-items">
+                    <li><a href="/page-components/overview" class="nav-item" sid="page-components-overview">Overview</a></li>
+                    <li><a href="/page-components/templates" class="nav-item" sid="page-components-templates">Page Templates</a></li>
+                </ul>
+            </div>
+            
+            <!-- Static Resources Section -->
+            <div class="nav-section" sid="static-resources-section">
+                <button class="nav-section-header" spry-action=":ToggleStaticResources" spry-target="static-resources-items">
+                    Static Resources
+                </button>
+                <ul class="nav-items" sid="static-resources-items">
+                    <li><a href="/static-resources/overview" class="nav-item" sid="static-resources-overview">Overview</a></li>
+                    <li><a href="/static-resources/spry-mkssr" class="nav-item" sid="static-resources-spry-mkssr">Using spry-mkssr</a></li>
+                </ul>
+            </div>
+        </aside>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Determine which sections should be expanded based on current path
+        _components_expanded = current_path.has_prefix("/components");
+        _page_components_expanded = current_path.has_prefix("/page-components");
+        _static_resources_expanded = current_path.has_prefix("/static-resources");
+        
+        // Apply expanded state to sections
+        update_section_state("components-section", "components-items", _components_expanded);
+        update_section_state("page-components-section", "page-components-items", _page_components_expanded);
+        update_section_state("static-resources-section", "static-resources-items", _static_resources_expanded);
+        
+        // Highlight the current page link
+        highlight_current_link();
+    }
+    
+    private void update_section_state(string section_sid, string items_sid, bool expanded) {
+        var section = this[section_sid];
+        var items = this[items_sid];
+        
+        if (section != null) {
+            if (expanded) {
+                section.add_class("expanded");
+            } else {
+                section.remove_class("expanded");
+            }
+        }
+        
+        if (items != null) {
+            if (expanded) {
+                items.remove_attribute("hidden");
+            } else {
+                items.set_attribute("hidden", "hidden");
+            }
+        }
+    }
+    
+    private void highlight_current_link() {
+        // Get the sid for the current path and add active class
+        string? sid = get_sid_for_path(current_path);
+        if (sid != null) {
+            var link = this[sid];
+            if (link != null) {
+                link.add_class("active");
+            }
+        }
+    }
+    
+    private string? get_sid_for_path(string path) {
+        switch (path) {
+            case "/": return "home-link";
+            case "/components/overview": return "components-overview";
+            case "/components/template-syntax": return "components-template-syntax";
+            case "/components/actions": return "components-actions";
+            case "/components/outlets": return "components-outlets";
+            case "/components/continuations": return "components-continuations";
+            case "/page-components/overview": return "page-components-overview";
+            case "/page-components/templates": return "page-components-templates";
+            case "/static-resources/overview": return "static-resources-overview";
+            case "/static-resources/spry-mkssr": return "static-resources-spry-mkssr";
+            default: return null;
+        }
+    }
+    
+    public override async void handle_action(string action) throws Error {
+        switch (action) {
+            case "ToggleComponents":
+                _components_expanded = !_components_expanded;
+                update_section_state("components-section", "components-items", _components_expanded);
+                break;
+            case "TogglePageComponents":
+                _page_components_expanded = !_page_components_expanded;
+                update_section_state("page-components-section", "page-components-items", _page_components_expanded);
+                break;
+            case "ToggleStaticResources":
+                _static_resources_expanded = !_static_resources_expanded;
+                update_section_state("static-resources-section", "static-resources-items", _static_resources_expanded);
+                break;
+        }
+    }
+}

+ 0 - 25
demo/Components/ParticleCanvasComponent.vala

@@ -1,25 +0,0 @@
-using Spry;
-
-/**
- * ParticleCanvasComponent - A single particle in the simulation
- * 
- * Renders as a colored circle at a specific position
- */
-public class ParticleCanvasComponent : Component {
-    
-    public double x { set; get; default = 50; }
-    public double y { set; get; default = 50; }
-    public string color { set; get; default = "#7c3aed"; }
-    public int size { set; get; default = 12; }
-    
-    public override string markup { get {
-        return """
-        <div sid="particle" style="position: absolute; border-radius: 50%; box-shadow: 0 0 10px currentColor;"></div>
-        """;
-    }}
-    
-    public override async void prepare() throws Error {
-        var style = @"position: absolute; left: $(x)%; top: $(y)%; width: $(size)px; height: $(size)px; background: $(color); border-radius: 50%; box-shadow: 0 0 $(size / 2)px $(color); transform: translate(-50%, -50%); transition: all 0.1s ease-out;";
-        this["particle"].set_attribute("style", style);
-    }
-}

+ 0 - 26
demo/Components/StatCardComponent.vala

@@ -1,26 +0,0 @@
-using Spry;
-
-/**
- * StatCardComponent - A statistics display card
- * 
- * Shows a large value with a label underneath
- */
-public class StatCardComponent : Component {
-    
-    public string value { set; get; default = "0"; }
-    public string label { set; get; default = "Stat"; }
-    
-    public override string markup { get {
-        return """
-        <div class="stat-card">
-            <div class="stat-value" sid="value"></div>
-            <div class="stat-label" sid="label"></div>
-        </div>
-        """;
-    }}
-    
-    public override async void prepare() throws Error {
-        this["value"].text_content = value;
-        this["label"].text_content = label;
-    }
-}

+ 123 - 0
demo/DemoComponents/ProgressDemo.vala

@@ -0,0 +1,123 @@
+using Spry;
+using Inversion;
+
+/**
+ * ProgressDemo - A progress bar demo for the Continuations documentation
+ * 
+ * Demonstrates:
+ * - spry-continuation attribute for SSE
+ * - spry-dynamic for updatable sections
+ * - continuation() method for real-time updates
+ * - ContinuationContext API
+ */
+public class ProgressDemo : Component {
+    
+    public int percent { get; set; default = 0; }
+    public string status { get; set; default = "Ready"; }
+    public bool is_running { get; set; default = false; }
+    
+    public override string markup { get {
+        return """
+        <div sid="progress" class="demo-progress">
+            <div class="progress-header">
+                <h3>Task Progress</h3>
+                <span sid="status-text" class="progress-status"></span>
+            </div>
+            <div class="progress-bar-container">
+                <div sid="progress-bar" class="progress-bar"
+                     style-width-expr='format("%i%%", this.percent)'>
+                </div>
+            </div>
+            <div class="progress-info">
+                <span sid="percent-text" class="progress-percent"></span>
+            </div>
+            <div class="progress-controls">
+                <button sid="start-btn" spry-action=":Start" spry-target="progress"
+                        class="progress-btn">Start Task</button>
+                <button sid="reset-btn" spry-action=":Reset" spry-target="progress"
+                        class="progress-btn secondary">Reset</button>
+            </div>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        this["status-text"].text_content = status;
+        this["percent-text"].text_content = @"$percent%";
+        
+        if (is_running) {
+            this["start-btn"].set_attribute("disabled", "disabled");
+            this["start-btn"].text_content = "Running...";
+        } else {
+            this["start-btn"].remove_attribute("disabled");
+            this["start-btn"].text_content = percent >= 100 ? "Complete!" : "Start Task";
+        }
+    }
+    
+    public async override void handle_action(string action) throws Error {
+        switch (action) {
+            case "Start":
+                if (!is_running && percent < 100) {
+                    is_running = true;
+                    status = "Starting...";
+                }
+                break;
+            case "Reset":
+                percent = 0;
+                status = "Ready";
+                is_running = false;
+                break;
+        }
+    }
+    
+    public async override void continuation(ContinuationContext ctx) throws Error {
+        if (!is_running) return;
+        
+        for (int i = percent; i <= 100; i += 5) {
+            percent = i;
+            
+            if (i < 30) {
+                status = @"Processing... $i%";
+            } else if (i < 60) {
+                status = @"Building... $i%";
+            } else if (i < 90) {
+                status = @"Finalizing... $i%";
+            } else {
+                status = @"Almost done... $i%";
+            }
+            
+            // Send updates via SSE
+            yield ctx.send_dynamic("progress");
+            
+            // Simulate work
+            Timeout.add(200, () => {
+                continuation.callback();
+                return false;
+            });
+            yield;
+        }
+        
+        percent = 100;
+        status = "Complete!";
+        is_running = false;
+        
+        yield ctx.send_dynamic("progress");
+    }
+}
+
+/**
+ * ProgressDemoWithSSE - Progress demo with SSE wrapper
+ * 
+ * This version includes the spry-continuation attribute wrapper
+ * for demonstrating SSE in the documentation.
+ */
+public class ProgressDemoWithSSE : Component {
+    
+    public override string markup { get {
+        return """
+        <div sid="wrapper" spry-continuation>
+            <spry-component name="ProgressDemo" sid="progress-demo" spry-dynamic="progress"/>
+        </div>
+        """;
+    }}
+}

+ 53 - 0
demo/DemoComponents/SimpleCounterDemo.vala

@@ -0,0 +1,53 @@
+using Spry;
+using Inversion;
+
+/**
+ * SimpleCounterDemo - A basic counter demo for the Actions documentation
+ * 
+ * Demonstrates:
+ * - spry-action for same-component actions
+ * - spry-target for scoped targeting
+ * - State management via a singleton store
+ */
+public class SimpleCounterDemo : Component {
+    
+    private CounterDemoStore store = inject<CounterDemoStore>();
+    
+    public override string markup { get {
+        return """
+        <div sid="counter" class="demo-counter">
+            <div class="counter-display" sid="display">0</div>
+            <div class="counter-controls">
+                <button class="counter-btn decrement" 
+                        spry-action=":Decrement" 
+                        spry-target="counter">−</button>
+                <button class="counter-btn increment" 
+                        spry-action=":Increment" 
+                        spry-target="counter">+</button>
+            </div>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        this["display"].text_content = store.count.to_string();
+    }
+    
+    public async override void handle_action(string action) throws Error {
+        switch (action) {
+            case "Increment":
+                store.count++;
+                break;
+            case "Decrement":
+                store.count--;
+                break;
+        }
+    }
+}
+
+/**
+ * CounterDemoStore - Singleton store to persist counter state
+ */
+public class CounterDemoStore : Object {
+    public int count { get; set; default = 0; }
+}

+ 170 - 0
demo/DemoComponents/TodoListDemo.vala

@@ -0,0 +1,170 @@
+using Spry;
+using Astralis;
+using Inversion;
+using Invercargill.DataStructures;
+
+/**
+ * TodoListDemo - A todo list demo for the Outlets documentation
+ * 
+ * Demonstrates:
+ * - Using spry-outlet for dynamic child components
+ * - set_outlet_children() to populate outlets
+ * - Parent/child component composition
+ */
+public class TodoListDemo : Component {
+    
+    private TodoDemoStore store = inject<TodoDemoStore>();
+    private ComponentFactory factory = inject<ComponentFactory>();
+    private HttpContext http_context = inject<HttpContext>();
+    
+    public override string markup { get {
+        return """
+        <div sid="todo-list" class="demo-todo-list">
+            <div class="todo-header">
+                <h3>Todo List</h3>
+                <span class="todo-count" sid="count"></span>
+            </div>
+            <form class="todo-form" spry-action=":Add" spry-target="todo-list">
+                <input type="text" name="title" placeholder="Add a task..." sid="input"/>
+                <button type="submit" class="todo-add-btn">Add</button>
+            </form>
+            <ul class="todo-items" sid="items">
+                <spry-outlet sid="items-outlet"/>
+            </ul>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Update count
+        this["count"].text_content = @"$(store.items.length) items";
+        
+        // Build list items
+        var children = new Series<Renderable>();
+        foreach (var item in store.items) {
+            var component = factory.create<TodoItemDemo>();
+            component.item_id = item.id;
+            children.add(component);
+        }
+        set_outlet_children("items-outlet", children);
+    }
+    
+    public async override void handle_action(string action) throws Error {
+        if (action == "Add") {
+            var title = http_context.request.query_params.get_any_or_default("title");
+            if (title != null && title.strip().length > 0) {
+                store.add(title.strip());
+            }
+        }
+    }
+}
+
+/**
+ * TodoItemDemo - Individual todo item component
+ */
+public class TodoItemDemo : Component {
+    
+    private TodoDemoStore store = inject<TodoDemoStore>();
+    private HttpContext http_context = inject<HttpContext>();
+    
+    public int item_id { set; get; }
+    
+    public override string markup { get {
+        return """
+        <li sid="item" class="todo-item">
+            <span sid="title" class="todo-title"></span>
+            <button sid="toggle" spry-action=":Toggle" spry-target="item" 
+                    class="todo-toggle"></button>
+            <button sid="delete" spry-action=":Delete" spry-target="item"
+                    hx-swap="delete" class="todo-delete">×</button>
+        </li>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        var item = store.get_by_id(item_id);
+        if (item == null) return;
+        
+        this["title"].text_content = item.title;
+        this["item"].set_attribute("hx-vals", @"{\"id\":$item_id}");
+        
+        if (item.completed) {
+            this["item"].add_class("completed");
+            this["toggle"].text_content = "↩";
+        } else {
+            this["toggle"].text_content = "✓";
+        }
+    }
+    
+    public async override void handle_action(string action) throws Error {
+        var id_str = http_context.request.query_params.get_any_or_default("id");
+        var id = int.parse(id_str);
+        
+        switch (action) {
+            case "Toggle":
+                store.toggle(id);
+                break;
+            case "Delete":
+                store.remove(id);
+                break;
+        }
+    }
+}
+
+/**
+ * TodoDemoStore - Singleton store for todo items
+ */
+public class TodoDemoStore : Object {
+    
+    public Series<TodoDemoItem> items { get; set; default = new Series<TodoDemoItem>(); }
+    private int _next_id = 1;
+    
+    public TodoDemoStore() {
+        // Add some initial items
+        add("Learn Spry components");
+        add("Build a web app");
+        add("Deploy to production");
+    }
+    
+    public void add(string title) {
+        items.add(new TodoDemoItem(_next_id++, title));
+    }
+    
+    public TodoDemoItem? get_by_id(int id) {
+        foreach (var item in items) {
+            if (item.id == id) return item;
+        }
+        return null;
+    }
+    
+    public void toggle(int id) {
+        var item = get_by_id(id);
+        if (item != null) {
+            item.completed = !item.completed;
+        }
+    }
+    
+    public void remove(int id) {
+        // Build a new list without the item
+        var new_items = new Series<TodoDemoItem>();
+        foreach (var item in items) {
+            if (item.id != id) {
+                new_items.add(item);
+            }
+        }
+        items = new_items;
+    }
+}
+
+/**
+ * TodoDemoItem - Single todo item data
+ */
+public class TodoDemoItem : Object {
+    public int id { get; construct; }
+    public string title { get; construct set; }
+    public bool completed { get; set; default = false; }
+    
+    public TodoDemoItem(int id, string title) {
+        Object(id: id, title: title);
+    }
+}

+ 49 - 4
demo/Main.vala

@@ -3,6 +3,7 @@ using Invercargill;
 using Invercargill.DataStructures;
 using Inversion;
 using Spry;
+using Demo.Static;
 
 void main(string[] args) {
     int port = args.length > 1 ? int.parse(args[1]) : 8080;
@@ -20,20 +21,64 @@ void main(string[] args) {
         var spry_cfg = application.configure_with<SpryConfigurator>();
         spry_cfg.add_template<MainTemplate>("");
 
+        // Register static resources
+        application.container.register_startup<DocsCssResource>()
+            .as<Spry.StaticResource>();
+
+        // Register demo stores (singletons for state persistence)
+        application.add_singleton<CounterDemoStore>();
+        application.add_singleton<TodoDemoStore>();
+        
         // Add Components
-        application.add_transient<AuroraWaveComponent>();
-        application.add_transient<FeatureCardComponent>();
         application.add_transient<CodeBlockComponent>();
-        application.add_transient<StatCardComponent>();
+        application.add_transient<DemoHostComponent>();
+        application.add_transient<NavSidebarComponent>();
+        
+        // Add Demo Components
+        application.add_transient<SimpleCounterDemo>();
+        application.add_transient<TodoListDemo>();
+        application.add_transient<TodoItemDemo>();
+        application.add_transient<ProgressDemo>();
+        application.add_transient<ProgressDemoWithSSE>();
         
         // Register page components
         application.add_transient<HomePage>();
         application.add_endpoint<HomePage>(new EndpointRoute("/"));
         
+        // Components documentation pages
+        application.add_transient<ComponentsOverviewPage>();
+        application.add_endpoint<ComponentsOverviewPage>(new EndpointRoute("/components/overview"));
+        
+        application.add_transient<ComponentsTemplateSyntaxPage>();
+        application.add_endpoint<ComponentsTemplateSyntaxPage>(new EndpointRoute("/components/template-syntax"));
+        
+        application.add_transient<ComponentsActionsPage>();
+        application.add_endpoint<ComponentsActionsPage>(new EndpointRoute("/components/actions"));
+        
+        application.add_transient<ComponentsOutletsPage>();
+        application.add_endpoint<ComponentsOutletsPage>(new EndpointRoute("/components/outlets"));
+        
+        application.add_transient<ComponentsContinuationsPage>();
+        application.add_endpoint<ComponentsContinuationsPage>(new EndpointRoute("/components/continuations"));
+        
+        // Page Components documentation pages
+        application.add_transient<PageComponentsOverviewPage>();
+        application.add_endpoint<PageComponentsOverviewPage>(new EndpointRoute("/page-components/overview"));
+        
+        application.add_transient<PageTemplatesPage>();
+        application.add_endpoint<PageTemplatesPage>(new EndpointRoute("/page-components/templates"));
+        
+        // Static Resources documentation pages
+        application.add_transient<StaticResourcesOverviewPage>();
+        application.add_endpoint<StaticResourcesOverviewPage>(new EndpointRoute("/static-resources/overview"));
+        
+        application.add_transient<StaticResourcesMkssrPage>();
+        application.add_endpoint<StaticResourcesMkssrPage>(new EndpointRoute("/static-resources/spry-mkssr"));
+        
         application.run();
         
     } catch (Error e) {
         printerr("Error: %s\n", e.message);
         Process.exit(1);
     }
-}
+}

+ 28 - 75
demo/MainTemplate.vala

@@ -1,95 +1,48 @@
 using Astralis;
 using Spry;
+using Inversion;
 
 /**
- * SiteLayoutTemplate - The base template for all pages
+ * MainTemplate - The base template for documentation pages
  * 
- * Provides:
- * - HTML document structure
- * - Common <head> elements (scripts, styles)
- * - Site-wide header with navigation
- * - Site-wide footer
+ * Provides a clean, minimal HTML structure for documentation:
+ * - HTML5 document structure
+ * - NavSidebarComponent for navigation
+ * - docs.css stylesheet for documentation styling
+ * - htmx.js and htmx-sse.js for interactive components
+ * - Template outlet for page content
  */
 public class MainTemplate : PageTemplate {
     
+    private NavSidebarComponent nav;
+    
     public override string markup { get {
         return """
         <!DOCTYPE html>
         <html lang="en">
         <head>
-            <meta charset="UTF-8">
-            <meta name="viewport" content="width=device-width, initial-scale=1.0">
-            <title>Spry Framework - Modern Web Development in Vala</title>
-            <meta name="description" content="Spry is a component-based web framework for Vala, featuring HTMX integration, dependency injection, and reactive templates.">
-            <link rel="stylesheet" href="/styles/main.css">
-            <script spry-res="htmx.js"></script>
-            <script spry-res="htmx-sse.js"></script>
+            <meta charset="utf-8">
+            <meta name="viewport" content="width=device-width, initial-scale=1">
+            <title>Spry Documentation</title>
+            <link rel="stylesheet" spry-res="docs.css">
         </head>
         <body>
-            <header class="site-header">
-                <div class="header-content">
-                    <a href="/" class="logo">
-                        <div class="logo-icon">⚡</div>
-                        Spry
-                    </a>
-                    <nav>
-                        <ul class="nav-links">
-                            <li><a href="/features">Features</a></li>
-                            <li><a href="/ecosystem">Ecosystem</a></li>
-                            <li><a href="/demo">Demo</a></li>
-                            <li><a href="/freedom">Freedom</a></li>
-                        </ul>
-                    </nav>
-                    <a href="/demo" class="nav-cta">Try Demo</a>
-                </div>
-            </header>
-            <main>
-                <spry-template-outlet />
-            </main>
-            <footer class="site-footer">
-                <div class="container">
-                    <div class="footer-content">
-                        <div class="footer-brand">
-                            <a href="/" class="logo">
-                                <div class="logo-icon">⚡</div>
-                                Spry
-                            </a>
-                            <p>A modern, component-based web framework for Vala. Build reactive, real-time web applications with ease.</p>
-                        </div>
-                        <div class="footer-section">
-                            <h4>Framework</h4>
-                            <ul class="footer-links">
-                                <li><a href="/features">Features</a></li>
-                                <li><a href="/ecosystem">Ecosystem</a></li>
-                                <li><a href="/demo">Live Demo</a></li>
-                            </ul>
-                        </div>
-                        <div class="footer-section">
-                            <h4>Ecosystem</h4>
-                            <ul class="footer-links">
-                                <li><a href="/ecosystem">Astralis</a></li>
-                                <li><a href="/ecosystem">Inversion</a></li>
-                                <li><a href="/ecosystem">Spry</a></li>
-                            </ul>
-                        </div>
-                        <div class="footer-section">
-                            <h4>Community</h4>
-                            <ul class="footer-links">
-                                <li><a href="/freedom">Free Software</a></li>
-                                <li><a href="https://github.com" rel="external">GitHub</a></li>
-                            </ul>
-                        </div>
-                    </div>
-                    <div class="footer-bottom">
-                        <p>© 2024 Spry Framework. Free and Open Source Software.</p>
-                        <div class="footer-social">
-                            <a href="https://github.com" title="GitHub">⌨</a>
-                        </div>
-                    </div>
-                </div>
-            </footer>
+            <div class="page-container">
+                <spry-component name="NavSidebarComponent" sid="nav"/>
+                <main class="main-content">
+                    <spry-template-outlet/>
+                </main>
+            </div>
+            <script spry-res="htmx.js"></script>
+            <script spry-res="htmx-sse.js"></script>
         </body>
         </html>
         """;
     }}
+    
+    public override async void prepare() throws Error {
+        // Get the nav component - it will use its default current_path
+        // Pages that need to set a specific current path should do so in their own prepare()
+        nav = get_component_child<NavSidebarComponent>("nav");
+    }
 }

+ 319 - 0
demo/Pages/ComponentsActionsPage.vala

@@ -0,0 +1,319 @@
+using Spry;
+using Inversion;
+
+/**
+ * ComponentsActionsPage - Documentation for Spry component actions
+ * 
+ * This page covers how actions work, the spry-action attribute syntax,
+ * and how to handle user interactions in components.
+ */
+public class ComponentsActionsPage : PageComponent {
+    
+    public const string ROUTE = "/components/actions";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div sid="page" class="doc-content">
+            <h1>Actions</h1>
+            
+            <section class="doc-section">
+                <p>
+                    Actions are the primary way to handle user interactions in Spry. When a user
+                    clicks a button, submits a form, or triggers any HTMX event, an action is sent
+                    to the server and handled by your component.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>How Actions Work</h2>
+                <p>
+                    The flow of an action request:
+                </p>
+                <ol>
+                    <li>User interacts with an element that has <code>spry-action</code></li>
+                    <li>HTMX sends a request to the Spry action endpoint</li>
+                    <li>Spry creates a <strong>new</strong> component instance and calls <code>handle_action()</code></li>
+                    <li>Your component modifies state in stores (not instance variables!)</li>
+                    <li>Spry calls <code>prepare()</code> to update the template from store data</li>
+                    <li>The updated HTML is sent back and swapped into the page</li>
+                </ol>
+                
+                <div class="warning-box">
+                    <p>
+                        <strong>⚠️ Critical:</strong> A <strong>new component instance</strong> is created for each action request.
+                        Any instance variables (like <code>is_on</code>, <code>count</code>) will NOT retain their values.
+                        Always store state in singleton stores, not component instance fields.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The <code>spry-action</code> Attribute</h2>
+                <p>
+                    The <code>spry-action</code> attribute specifies which action to trigger. There are
+                    two syntaxes depending on whether you're targeting the same component or a different one.
+                </p>
+                
+                <h3>Same-Component Actions</h3>
+                <p>
+                    Use <code>:ActionName</code> to trigger an action on the same component type that contains
+                    the element.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="same-action-html"/>
+                <spry-component name="CodeBlockComponent" sid="same-action-vala"/>
+                
+                <h3>Cross-Component Actions</h3>
+                <p>
+                    Use <code>ComponentName:ActionName</code> to trigger an action on a different component type.
+                    This is useful when an interaction in one component should update another.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="cross-action-html"/>
+                <spry-component name="CodeBlockComponent" sid="cross-action-vala"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The <code>spry-target</code> Attribute</h2>
+                <p>
+                    The <code>spry-target</code> attribute specifies which element should be replaced
+                    when the action completes. It uses the <code>sid</code> of an element within the
+                    same component.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="target-example"/>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 Tip:</strong> <code>spry-target</code> only works within the same component.
+                        For cross-component targeting, use <code>hx-target="#element-id"</code> with a
+                        global <code>id</code> attribute.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Passing Data with <code>hx-vals</code></h2>
+                <p>
+                    Since a new component instance is created for each request, you need to pass data
+                    explicitly using <code>hx-vals</code>. This data becomes available in query parameters,
+                    allowing you to identify which item to operate on.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="hx-vals-html"/>
+                <spry-component name="CodeBlockComponent" sid="hx-vals-vala"/>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 Tip:</strong> Set <code>hx-vals</code> on a parent element - it will be
+                        inherited by all child elements. This avoids repetition and keeps your code clean.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Swap Strategies</h2>
+                <p>
+                    Control how the response is inserted with <code>hx-swap</code>:
+                </p>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Value</th>
+                            <th>Description</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td><code>outerHTML</code></td>
+                            <td>Replace the entire target element (default for most cases)</td>
+                        </tr>
+                        <tr>
+                            <td><code>innerHTML</code></td>
+                            <td>Replace only the content inside the target</td>
+                        </tr>
+                        <tr>
+                            <td><code>delete</code></td>
+                            <td>Remove the target element (no response content needed)</td>
+                        </tr>
+                        <tr>
+                            <td><code>beforebegin</code></td>
+                            <td>Insert content before the target element</td>
+                        </tr>
+                        <tr>
+                            <td><code>afterend</code></td>
+                            <td>Insert content after the target element</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Complete Example: Toggle with Store</h2>
+                <p>
+                    Here's a complete example showing proper state management using a store.
+                    The store is a singleton that persists state across requests:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="store-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Live Demo: Counter</h2>
+                <p>
+                    Try this interactive counter to see actions in action. The counter state
+                    is stored in a singleton store, so it persists between requests:
+                </p>
+                
+                <spry-component name="DemoHostComponent" sid="counter-demo"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/components/outlets" class="nav-card">
+                        <h3>Outlets →</h3>
+                        <p>Compose components with outlets</p>
+                    </a>
+                    <a href="/components/continuations" class="nav-card">
+                        <h3>Continuations →</h3>
+                        <p>Real-time updates with SSE</p>
+                    </a>
+                    <a href="/components/template-syntax" class="nav-card">
+                        <h3>Template Syntax ←</h3>
+                        <p>Review template attributes</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Same-component action examples
+        var same_action_html = get_component_child<CodeBlockComponent>("same-action-html");
+        same_action_html.language = "HTML";
+        same_action_html.code = "<div sid=\"item\">\n" +
+            "    <span sid=\"status\">Off</span>\n" +
+            "    <button spry-action=\":Toggle\" spry-target=\"item\">Toggle</button>\n" +
+            "</div>";
+        
+        var same_action_vala = get_component_child<CodeBlockComponent>("same-action-vala");
+        same_action_vala.language = "Vala";
+        same_action_vala.code = "public async override void handle_action(string action) throws Error {\n" +
+            "    // Get item ID from hx-vals (passed via query params)\n" +
+            "    var id = get_id_from_query_params();\n\n" +
+            "    if (action == \"Toggle\") {\n" +
+            "        // Modify state in the STORE, not instance variables!\n" +
+            "        store.toggle(id);\n" +
+            "    }\n" +
+            "    // prepare() is called automatically to update template from store\n" +
+            "}";
+        
+        // Cross-component action examples
+        var cross_action_html = get_component_child<CodeBlockComponent>("cross-action-html");
+        cross_action_html.language = "HTML";
+        cross_action_html.code = "<!-- In AddFormComponent -->\n" +
+            "<form spry-action=\"TodoList:Add\" hx-target=\"#todo-list\" hx-swap=\"outerHTML\">\n" +
+            "    <input name=\"title\" type=\"text\"/>\n" +
+            "    <button type=\"submit\">Add</button>\n" +
+            "</form>";
+        
+        var cross_action_vala = get_component_child<CodeBlockComponent>("cross-action-vala");
+        cross_action_vala.language = "Vala";
+        cross_action_vala.code = "// In TodoListComponent\n" +
+            "public async override void handle_action(string action) throws Error {\n" +
+            "    if (action == \"Add\") {\n" +
+            "        var title = http_context.request.query_params.get_any_or_default(\"title\");\n" +
+            "        // Modify state in the store\n" +
+            "        store.add(title);\n" +
+            "    }\n" +
+            "    // prepare() rebuilds the list from store data\n" +
+            "}";
+        
+        // Target example
+        var target_example = get_component_child<CodeBlockComponent>("target-example");
+        target_example.language = "HTML";
+        target_example.code = "<div sid=\"item\" class=\"item\">\n" +
+            "    <span sid=\"title\">Item Title</span>\n" +
+            "    <button spry-action=\":Delete\" spry-target=\"item\" hx-swap=\"delete\">\n" +
+            "        Delete\n" +
+            "    </button>\n" +
+            "</div>";
+        
+        // hx-vals examples
+        var hx_vals_html = get_component_child<CodeBlockComponent>("hx-vals-html");
+        hx_vals_html.language = "HTML";
+        hx_vals_html.code = "<div sid=\"item\" hx-vals='{\"id\": 42}'>\n" +
+            "    <!-- Children inherit hx-vals -->\n" +
+            "    <button spry-action=\":Toggle\" spry-target=\"item\">Toggle</button>\n" +
+            "    <button spry-action=\":Delete\" spry-target=\"item\">Delete</button>\n" +
+            "</div>";
+        
+        var hx_vals_vala = get_component_child<CodeBlockComponent>("hx-vals-vala");
+        hx_vals_vala.language = "Vala";
+        hx_vals_vala.code = "public async override void handle_action(string action) throws Error {\n" +
+            "    // Get the id from query params (passed via hx-vals)\n" +
+            "    var id_str = http_context.request.query_params.get_any_or_default(\"id\");\n" +
+            "    var id = int.parse(id_str);\n\n" +
+            "    // Use the id to find and modify the item IN THE STORE\n" +
+            "    switch (action) {\n" +
+            "        case \"Toggle\":\n" +
+            "            store.toggle(id);\n" +
+            "            break;\n" +
+            "        case \"Delete\":\n" +
+            "            store.remove(id);\n" +
+            "            break;\n" +
+            "    }\n" +
+            "}";
+        
+        // Complete example with store
+        var store_example = get_component_child<CodeBlockComponent>("store-example");
+        store_example.language = "Vala";
+        store_example.code = "// First, define a store (singleton) to hold state\n" +
+            "public class ToggleStore : Object {\n" +
+            "    private bool _is_on = false;\n" +
+            "    public bool is_on { get { return _is_on; } }\n\n" +
+            "    public void toggle() {\n" +
+            "        _is_on = !_is_on;\n" +
+            "    }\n" +
+            "}\n\n" +
+            "// Register as singleton in your app:\n" +
+            "// application.add_singleton<ToggleStore>();\n\n" +
+            "// Then use it in your component:\n" +
+            "public class ToggleComponent : Component {\n" +
+            "    private ToggleStore store = inject<ToggleStore>();  // Inject singleton\n\n" +
+            "    public override string markup { get {\n" +
+            "        return \"\"\"\n" +
+            "        <div sid=\"toggle\" class=\"toggle-container\">\n" +
+            "            <span sid=\"status\" class=\"status\"></span>\n" +
+            "            <button sid=\"btn\" spry-action=\":Toggle\" spry-target=\"toggle\">\n" +
+            "            </button>\n" +
+            "        </div>\n" +
+            "        \"\"\";\n" +
+            "    }}\n\n" +
+            "    public override void prepare() throws Error {\n" +
+            "        // Read state from store\n" +
+            "        this[\"status\"].text_content = store.is_on ? \"ON\" : \"OFF\";\n" +
+            "        this[\"btn\"].text_content = store.is_on ? \"Turn Off\" : \"Turn On\";\n" +
+            "    }\n\n" +
+            "    public async override void handle_action(string action) throws Error {\n" +
+            "        if (action == \"Toggle\") {\n" +
+            "            // Modify state in the store - this persists!\n" +
+            "            store.toggle();\n" +
+            "        }\n" +
+            "    }\n" +
+            "}";
+        
+        // Set up the counter demo
+        var demo = get_component_child<DemoHostComponent>("counter-demo");
+        demo.source_file = "demo/DemoComponents/SimpleCounterDemo.vala";
+        
+        // Create and set the actual demo component
+        var counter_demo = factory.create<SimpleCounterDemo>();
+        demo.set_outlet_child("demo-outlet", counter_demo);
+    }
+}

+ 269 - 0
demo/Pages/ComponentsContinuationsPage.vala

@@ -0,0 +1,269 @@
+using Spry;
+using Inversion;
+
+/**
+ * ComponentsContinuationsPage - Documentation for Spry continuations (SSE)
+ * 
+ * This page covers what continuations are, the spry-continuation attribute,
+ * and how to send real-time updates to clients.
+ */
+public class ComponentsContinuationsPage : PageComponent {
+    
+    public const string ROUTE = "/components/continuations";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div sid="page" class="doc-content">
+            <h1>Continuations</h1>
+            
+            <section class="doc-section">
+                <p>
+                    Continuations enable real-time updates from server to client using Server-Sent Events (SSE).
+                    They're perfect for progress indicators, live status updates, and any scenario where
+                    the server needs to push updates to the client over time.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>What are Continuations?</h2>
+                <p>
+                    A continuation is a long-running server process that can send multiple updates
+                    to the client over a single HTTP connection. Unlike regular requests that return
+                    once, continuations can push updates as they happen.
+                </p>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 Use Cases:</strong> Progress bars, build status, live logs,
+                        real-time notifications, file upload progress, long-running task monitoring.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The <code>spry-continuation</code> Attribute</h2>
+                <p>
+                    Add <code>spry-continuation</code> to an element to enable SSE for its children.
+                    This attribute is shorthand for:
+                </p>
+                <ul>
+                    <li><code>hx-ext="sse"</code> - Enable HTMX SSE extension</li>
+                    <li><code>sse-connect="(auto-endpoint)"</code> - Connect to SSE endpoint</li>
+                    <li><code>sse-close="_spry-close"</code> - Close event name</li>
+                </ul>
+                
+                <spry-component name="CodeBlockComponent" sid="continuation-attr"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The <code>spry-dynamic</code> Attribute</h2>
+                <p>
+                    Mark child elements with <code>spry-dynamic="name"</code> to make them updatable.
+                    When you call <code>send_dynamic("name")</code>, that element's HTML is sent to
+                    the client and swapped in place.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="dynamic-attr"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The <code>continuation()</code> Method</h2>
+                <p>
+                    Override <code>continuation(ContinuationContext context)</code> in your component
+                    to implement long-running processes that send updates.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="continuation-method"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>ContinuationContext API</h2>
+                <p>
+                    The <code>ContinuationContext</code> parameter provides methods for sending updates:
+                </p>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Method</th>
+                            <th>Description</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td><code>send_dynamic(name)</code></td>
+                            <td>Send a dynamic section (by spry-dynamic name) as an SSE event</td>
+                        </tr>
+                        <tr>
+                            <td><code>send_json(event_type, node)</code></td>
+                            <td>Send JSON data as an SSE event</td>
+                        </tr>
+                        <tr>
+                            <td><code>send_string(event_type, data)</code></td>
+                            <td>Send raw string data as an SSE event</td>
+                        </tr>
+                        <tr>
+                            <td><code>send_full_update(event_type)</code></td>
+                            <td>Send the entire component document</td>
+                        </tr>
+                        <tr>
+                            <td><code>send_event(event)</code></td>
+                            <td>Send a custom SseEvent</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Required Scripts</h2>
+                <p>
+                    Include the HTMX SSE extension in your page for continuations to work:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="scripts"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The <code>spry-unique</code> Attribute</h2>
+                <p>
+                    Use <code>spry-unique</code> on elements that need stable IDs for targeting.
+                    Spry generates unique IDs automatically.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="unique-attr"/>
+                
+                <div class="warning-box">
+                    <p>
+                        <strong>⚠️ Restrictions:</strong> Cannot use <code>spry-unique</code> with an
+                        explicit <code>id</code> attribute, or inside <code>spry-per-*</code> loops.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Cancellation Handling</h2>
+                <p>
+                    Override <code>continuation_canceled()</code> to clean up resources when the
+                    client disconnects:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="canceled-method"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Live Demo: Progress Bar</h2>
+                <p>
+                    This demo shows a progress bar that updates in real-time using SSE.
+                    Click "Start Task" to see continuations in action!
+                </p>
+                
+                <spry-component name="DemoHostComponent" sid="progress-demo"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/page-components/overview" class="nav-card">
+                        <h3>Page Components →</h3>
+                        <p>Learn about page-level components</p>
+                    </a>
+                    <a href="/components/outlets" class="nav-card">
+                        <h3>Outlets ←</h3>
+                        <p>Component composition</p>
+                    </a>
+                    <a href="/components/actions" class="nav-card">
+                        <h3>Actions ←</h3>
+                        <p>Handle user interactions</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // spry-continuation attribute example
+        var continuation_attr = get_component_child<CodeBlockComponent>("continuation-attr");
+        continuation_attr.language = "HTML";
+        continuation_attr.code = "<div spry-continuation>\n" +
+            "    <!-- Children can receive SSE updates -->\n" +
+            "    <div spry-dynamic=\"progress-bar\">\n" +
+            "        <div class=\"progress-bar\" style-width-expr='format(\"%i%%\", this.percent)'>\n" +
+            "        </div>\n" +
+            "    </div>\n" +
+            "    <div spry-dynamic=\"status\">\n" +
+            "        <strong>Status:</strong> <span spry-unique content-expr=\"this.status\">Ready</span>\n" +
+            "    </div>\n" +
+            "</div>";
+        
+        // spry-dynamic attribute example
+        var dynamic_attr = get_component_child<CodeBlockComponent>("dynamic-attr");
+        dynamic_attr.language = "HTML";
+        dynamic_attr.code = "<!-- spry-dynamic marks elements for SSE swapping -->\n" +
+            "<div class=\"progress-container\" spry-dynamic=\"progress-bar\">\n" +
+            "    <div class=\"progress-bar\">...</div>\n" +
+            "</div>\n\n" +
+            "<div class=\"status\" spry-dynamic=\"status\">\n" +
+            "    <strong>Status:</strong> <span>Processing...</span>\n" +
+            "</div>\n\n" +
+            "<!-- Automatically gets: sse-swap=\"_spry-dynamic-{name}\" hx-swap=\"outerHTML\" -->";
+        
+        // continuation() method example
+        var continuation_method = get_component_child<CodeBlockComponent>("continuation-method");
+        continuation_method.language = "Vala";
+        continuation_method.code = "public async override void continuation(ContinuationContext ctx) throws Error {\n" +
+            "    for (int i = 0; i <= 100; i += 10) {\n" +
+            "        percent = i;\n" +
+            "        status = @\"Processing... $(i)%\";\n\n" +
+            "        // Send dynamic section updates to the client\n" +
+            "        yield ctx.send_dynamic(\"progress-bar\");\n" +
+            "        yield ctx.send_dynamic(\"status\");\n\n" +
+            "        // Simulate async work\n" +
+            "        Timeout.add(500, () => {\n" +
+            "            continuation.callback();\n" +
+            "            return false;\n" +
+            "        });\n" +
+            "        yield;\n" +
+            "    }\n\n" +
+            "    status = \"Complete!\";\n" +
+            "    yield ctx.send_dynamic(\"status\");\n" +
+            "}";
+        
+        // Required scripts example
+        var scripts = get_component_child<CodeBlockComponent>("scripts");
+        scripts.language = "HTML";
+        scripts.code = "<!-- In your page template or component -->\n" +
+            "<script spry-res=\"htmx.js\"></script>\n" +
+            "<script spry-res=\"htmx-sse.js\"></script>";
+        
+        // spry-unique attribute example
+        var unique_attr = get_component_child<CodeBlockComponent>("unique-attr");
+        unique_attr.language = "HTML";
+        unique_attr.code = "<!-- spry-unique generates a stable unique ID -->\n" +
+            "<div class=\"progress-bar\" spry-unique>\n" +
+            "    <!-- Gets ID like: _spry-unique-0-abc123 -->\n" +
+            "</div>\n\n" +
+            "<!-- Use with content-expr for dynamic content -->\n" +
+            "<span spry-unique content-expr=\"this.status\">Loading...</span>";
+        
+        // continuation_canceled example
+        var canceled_method = get_component_child<CodeBlockComponent>("canceled-method");
+        canceled_method.language = "Vala";
+        canceled_method.code = "public async override void continuation_canceled() throws Error {\n" +
+            "    // Client disconnected - clean up resources\n" +
+            "    cancel_background_task();\n" +
+            "    release_file_handles();\n" +
+            "    log_message(\"Task canceled by client\");\n" +
+            "}";
+        
+        // Set up the demo host
+        var demo = get_component_child<DemoHostComponent>("progress-demo");
+        demo.source_file = "demo/DemoComponents/ProgressDemo.vala";
+        
+        // Create and set the actual demo component
+        var progress_demo = factory.create<ProgressDemoWithSSE>();
+        demo.set_outlet_child("demo-outlet", progress_demo);
+    }
+}

+ 271 - 0
demo/Pages/ComponentsOutletsPage.vala

@@ -0,0 +1,271 @@
+using Spry;
+using Inversion;
+
+/**
+ * ComponentsOutletsPage - Documentation for Spry outlets
+ * 
+ * This page covers what outlets are, how to use them for component
+ * composition, and the parent/child component pattern.
+ */
+public class ComponentsOutletsPage : PageComponent {
+    
+    public const string ROUTE = "/components/outlets";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div sid="page" class="doc-content">
+            <h1>Outlets</h1>
+            
+            <section class="doc-section">
+                <p>
+                    Outlets are placeholders in your component templates where child components
+                    can be dynamically inserted. They enable powerful component composition
+                    patterns, especially for lists and dynamic content.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>What are Outlets?</h2>
+                <p>
+                    An outlet is a special element in your markup that acts as a insertion point
+                    for child components. Think of it like a socket where you can plug in
+                    different components at runtime.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="outlet-syntax"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Using <code>set_outlet_children()</code></h2>
+                <p>
+                    The <code>set_outlet_children(sid, children)</code> method populates an outlet
+                    with child components. Call it in your <code>prepare()</code> method after
+                    creating child components with the ComponentFactory.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="set-outlet-vala"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Parent/Child Component Pattern</h2>
+                <p>
+                    The typical pattern for outlets involves:
+                </p>
+                <ol>
+                    <li>A <strong>parent component</strong> with an outlet and a method to set children</li>
+                    <li>A <strong>child component</strong> that displays individual items</li>
+                    <li>A <strong>store</strong> that holds the data</li>
+                </ol>
+                
+                <h3>Parent Component</h3>
+                <spry-component name="CodeBlockComponent" sid="parent-component"/>
+                
+                <h3>Child Component</h3>
+                <spry-component name="CodeBlockComponent" sid="child-component"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Creating Lists with Outlets</h2>
+                <p>
+                    Outlets are perfect for rendering dynamic lists. Here's the complete pattern:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="list-pattern"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Outlets vs <code>&lt;spry-component&gt;</code></h2>
+                <p>
+                    When should you use outlets vs declarative child components?
+                </p>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Feature</th>
+                            <th><code>&lt;spry-outlet&gt;</code></th>
+                            <th><code>&lt;spry-component&gt;</code></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td>Use Case</td>
+                            <td>Dynamic lists, multiple items</td>
+                            <td>Single, known child components</td>
+                        </tr>
+                        <tr>
+                            <td>Population</td>
+                            <td><code>set_outlet_children()</code> in prepare()</td>
+                            <td>Automatic, configured via properties</td>
+                        </tr>
+                        <tr>
+                            <td>Access</td>
+                            <td>Not directly accessible</td>
+                            <td><code>get_component_child<T>()</code></td>
+                        </tr>
+                        <tr>
+                            <td>Examples</td>
+                            <td>Todo lists, data tables, feeds</td>
+                            <td>Headers, sidebars, fixed sections</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Live Demo: Todo List</h2>
+                <p>
+                    This interactive demo shows outlets in action. The todo list uses an outlet
+                    to render individual todo items. Try adding, toggling, and deleting items!
+                </p>
+                
+                <spry-component name="DemoHostComponent" sid="todo-demo"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/components/continuations" class="nav-card">
+                        <h3>Continuations →</h3>
+                        <p>Real-time updates with SSE</p>
+                    </a>
+                    <a href="/components/actions" class="nav-card">
+                        <h3>Actions ←</h3>
+                        <p>Handle user interactions</p>
+                    </a>
+                    <a href="/components/template-syntax" class="nav-card">
+                        <h3>Template Syntax ←</h3>
+                        <p>Review template attributes</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Outlet syntax example
+        var outlet_syntax = get_component_child<CodeBlockComponent>("outlet-syntax");
+        outlet_syntax.language = "HTML";
+        outlet_syntax.code = "<div sid=\"list\">\n" +
+            "    <!-- This outlet will be filled with child components -->\n" +
+            "    <spry-outlet sid=\"items\"/>\n" +
+            "</div>";
+        
+        // set_outlet_children example
+        var set_outlet_vala = get_component_child<CodeBlockComponent>("set-outlet-vala");
+        set_outlet_vala.language = "Vala";
+        set_outlet_vala.code = "public override async void prepare() throws Error {\n" +
+            "    var factory = inject<ComponentFactory>();\n" +
+            "    var store = inject<TodoStore>();\n\n" +
+            "    // Create child components for each item\n" +
+            "    var children = new Series<Renderable>();\n" +
+            "    foreach (var item in store.items) {\n" +
+            "        var component = factory.create<TodoItemComponent>();\n" +
+            "        component.item_id = item.id;  // Pass data to child\n" +
+            "        children.add(component);\n" +
+            "    }\n\n" +
+            "    // Populate the outlet\n" +
+            "    set_outlet_children(\"items\", children);\n" +
+            "}";
+        
+        // Parent component example
+        var parent_component = get_component_child<CodeBlockComponent>("parent-component");
+        parent_component.language = "Vala";
+        parent_component.code = "public class TodoListComponent : Component {\n" +
+            "    private TodoStore store = inject<TodoStore>();\n" +
+            "    private ComponentFactory factory = inject<ComponentFactory>();\n\n" +
+            "    public override string markup { get {\n" +
+            "        return \"\"\"\n" +
+            "        <div sid=\"list\" class=\"todo-list\">\n" +
+            "            <ul sid=\"items\">\n" +
+            "                <spry-outlet sid=\"items-outlet\"/>\n" +
+            "            </ul>\n" +
+            "        </div>\n" +
+            "        \"\"\";\n" +
+            "    }}\n\n" +
+            "    public override async void prepare() throws Error {\n" +
+            "        var children = new Series<Renderable>();\n" +
+            "        foreach (var item in store.items) {\n" +
+            "            var component = factory.create<TodoItemComponent>();\n" +
+            "            component.item_id = item.id;\n" +
+            "            children.add(component);\n" +
+            "        }\n" +
+            "        set_outlet_children(\"items-outlet\", children);\n" +
+            "    }\n" +
+            "}";
+        
+        // Child component example
+        var child_component = get_component_child<CodeBlockComponent>("child-component");
+        child_component.language = "Vala";
+        child_component.code = "public class TodoItemComponent : Component {\n" +
+            "    private TodoStore store = inject<TodoStore>();\n" +
+            "    private HttpContext http_context = inject<HttpContext>();\n\n" +
+            "    public int item_id { get; set; }  // Set by parent\n\n" +
+            "    public override string markup { get {\n" +
+            "        return \"\"\"\n" +
+            "        <li sid=\"item\" class=\"todo-item\">\n" +
+            "            <span sid=\"title\"></span>\n" +
+            "            <button spry-action=\":Toggle\" spry-target=\"item\">Toggle</button>\n" +
+            "        </li>\n" +
+            "        \"\"\";\n" +
+            "    }}\n\n" +
+            "    public override void prepare() throws Error {\n" +
+            "        var item = store.get_by_id(item_id);\n" +
+            "        this[\"title\"].text_content = item.title;\n" +
+            "        // Pass item_id via hx-vals for handle_action\n" +
+            "        this[\"item\"].set_attribute(\"hx-vals\", @\"{\\\"id\\\":$item_id}\");\n" +
+            "    }\n\n" +
+            "    public async override void handle_action(string action) throws Error {\n" +
+            "        var id = int.parse(http_context.request.query_params.get_any_or_default(\"id\"));\n" +
+            "        if (action == \"Toggle\") {\n" +
+            "            store.toggle(id);\n" +
+            "        }\n" +
+            "    }\n" +
+            "}";
+        
+        // List pattern example
+        var list_pattern = get_component_child<CodeBlockComponent>("list-pattern");
+        list_pattern.language = "Vala";
+        list_pattern.code = "// 1. Define a store to hold data\n" +
+            "public class TodoStore : Object {\n" +
+            "    public Series<TodoItem> items { get; default = new Series<TodoItem>(); }\n\n" +
+            "    public void add(string title) { /* ... */ }\n" +
+            "    public void toggle(int id) { /* ... */ }\n" +
+            "    public void remove(int id) { /* ... */ }\n" +
+            "}\n\n" +
+            "// 2. Register as singleton\n" +
+            "application.add_singleton<TodoStore>();\n\n" +
+            "// 3. Parent component uses outlet\n" +
+            "public class TodoListComponent : Component {\n" +
+            "    private TodoStore store = inject<TodoStore>();\n\n" +
+            "    public override string markup { get {\n" +
+            "        return \"<ul><spry-outlet sid=\"items\"/></ul>\";\n" +
+            "    }}\n\n" +
+            "    public override async void prepare() throws Error {\n" +
+            "        var children = new Series<Renderable>();\n" +
+            "        foreach (var item in store.items) {\n" +
+            "            var child = factory.create<TodoItemComponent>();\n" +
+            "            child.item_id = item.id;\n" +
+            "            children.add(child);\n" +
+            "        }\n" +
+            "        set_outlet_children(\"items\", children);\n" +
+            "    }\n" +
+            "}\n\n" +
+            "// 4. Child component handles individual items\n" +
+            "public class TodoItemComponent : Component {\n" +
+            "    public int item_id { get; set; }\n" +
+            "    // ... markup and methods\n" +
+            "}";
+        
+        // Set up the demo host
+        var demo = get_component_child<DemoHostComponent>("todo-demo");
+        demo.source_file = "demo/DemoComponents/TodoListDemo.vala";
+        
+        // Create and set the actual demo component
+        var todo_demo = factory.create<TodoListDemo>();
+        demo.set_outlet_child("demo-outlet", todo_demo);
+    }
+}

+ 203 - 0
demo/Pages/ComponentsOverviewPage.vala

@@ -0,0 +1,203 @@
+using Spry;
+using Inversion;
+
+/**
+ * ComponentsOverviewPage - Introduction to Spry Components
+ * 
+ * This page provides a comprehensive overview of what Spry components are,
+ * their lifecycle, key methods, and basic usage patterns.
+ */
+public class ComponentsOverviewPage : PageComponent {
+    
+    public const string ROUTE = "/components/overview";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div sid="page" class="doc-content">
+            <h1>Components Overview</h1>
+            
+            <section class="doc-section">
+                <h2>What are Spry Components?</h2>
+                <p>
+                    Spry components are the building blocks of your web application. They combine
+                    HTML templates with behavior logic in a single, cohesive Vala class. Each component
+                    is self-contained with its own markup, state management, and action handling.
+                </p>
+                <p>
+                    Unlike traditional web frameworks where templates and logic are separated,
+                    Spry components embrace a component-based architecture where everything related
+                    to a UI element lives in one place.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Component Lifecycle</h2>
+                <p>
+                    Understanding the component lifecycle is crucial for building effective Spry applications.
+                    The template DOM is <strong>fresh on every request</strong>, meaning state is not
+                    persisted between requests.
+                </p>
+                
+                <div class="lifecycle-diagram">
+                    <div class="lifecycle-step">
+                        <div class="step-number">1</div>
+                        <div class="step-content">
+                            <h4>Component Creation</h4>
+                            <p>Component is instantiated via ComponentFactory</p>
+                        </div>
+                    </div>
+                    <div class="lifecycle-arrow">↓</div>
+                    <div class="lifecycle-step">
+                        <div class="step-number">2</div>
+                        <div class="step-content">
+                            <h4>prepare_once()</h4>
+                            <p>One-time initialization (called only before first prepare)</p>
+                        </div>
+                    </div>
+                    <div class="lifecycle-arrow">↓</div>
+                    <div class="lifecycle-step">
+                        <div class="step-number">3</div>
+                        <div class="step-content">
+                            <h4>prepare()</h4>
+                            <p>Fetch data from stores, populate template elements</p>
+                        </div>
+                    </div>
+                    <div class="lifecycle-arrow">↓</div>
+                    <div class="lifecycle-step">
+                        <div class="step-number">4</div>
+                        <div class="step-content">
+                            <h4>handle_action()</h4>
+                            <p>Handle user interactions (if action triggered)</p>
+                        </div>
+                    </div>
+                    <div class="lifecycle-arrow">↓</div>
+                    <div class="lifecycle-step">
+                        <div class="step-number">5</div>
+                        <div class="step-content">
+                            <h4>Serialization</h4>
+                            <p>Template is rendered to HTML and sent to client</p>
+                        </div>
+                    </div>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Key Methods</h2>
+                <p>
+                    Every component inherits from the base <code>Component</code> class and can
+                    override these key methods:
+                </p>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Method</th>
+                            <th>Description</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td><code>markup</code></td>
+                            <td>Property returning the HTML template string with Spry attributes</td>
+                        </tr>
+                        <tr>
+                            <td><code>prepare()</code></td>
+                            <td>Called before serialization. Fetch data and populate template here.</td>
+                        </tr>
+                        <tr>
+                            <td><code>prepare_once()</code></td>
+                            <td>Called only once before the first prepare(). For one-time initialization.</td>
+                        </tr>
+                        <tr>
+                            <td><code>handle_action()</code></td>
+                            <td>Called when HTMX triggers an action. Modify state here.</td>
+                        </tr>
+                        <tr>
+                            <td><code>continuation()</code></td>
+                            <td>For real-time updates via Server-Sent Events (SSE).</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Basic Component Example</h2>
+                <p>
+                    Here's a simple counter component demonstrating the core concepts:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="example-code"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Important: Template State is Not Persisted</h2>
+                <div class="warning-box">
+                    <p>
+                        <strong>⚠️ Critical Concept:</strong> The template DOM is fresh on every request.
+                        Any state set via setters (like <code>title</code>, <code>completed</code>) is 
+                        NOT available in <code>handle_action()</code>.
+                    </p>
+                    <p>
+                        Always use <code>prepare()</code> to fetch data from stores and set up the template.
+                        Use <code>hx-vals</code> to pass data between requests.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/components/template-syntax" class="nav-card">
+                        <h3>Template Syntax →</h3>
+                        <p>Learn about Spry's special HTML attributes</p>
+                    </a>
+                    <a href="/components/actions" class="nav-card">
+                        <h3>Actions →</h3>
+                        <p>Handle user interactions with actions</p>
+                    </a>
+                    <a href="/components/outlets" class="nav-card">
+                        <h3>Outlets →</h3>
+                        <p>Compose components with outlets</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        var code = get_component_child<CodeBlockComponent>("example-code");
+        code.language = "Vala";
+        code.code = "using Spry;\n\n" +
+            "public class CounterComponent : Component {\n" +
+            "    private CounterStore store = inject<CounterStore>();\n\n" +
+            "    public override string markup { get {\n" +
+            "        return \"\"\"\n" +
+            "        <div sid=\"counter\" class=\"counter\">\n" +
+            "            <span sid=\"count\"></span>\n" +
+            "            <button sid=\"inc\" spry-action=\":Increment\"\n" +
+            "                    spry-target=\"counter\">+</button>\n" +
+            "            <button sid=\"dec\" spry-action=\":Decrement\"\n" +
+            "                    spry-target=\"counter\">-</button>\n" +
+            "        </div>\n" +
+            "        \"\"\";\n" +
+            "    }}\n\n" +
+            "    public override void prepare() throws Error {\n" +
+            "        this[\"count\"].text_content = store.count.to_string();\n" +
+            "    }\n\n" +
+            "    public async override void handle_action(string action) throws Error {\n" +
+            "        switch (action) {\n" +
+            "            case \"Increment\":\n" +
+            "                store.count++;\n" +
+            "                break;\n" +
+            "            case \"Decrement\":\n" +
+            "                store.count--;\n" +
+            "                break;\n" +
+            "        }\n" +
+            "        // prepare() is called automatically after handle_action()\n" +
+            "    }\n" +
+            "}";
+    }
+}

+ 272 - 0
demo/Pages/ComponentsTemplateSyntaxPage.vala

@@ -0,0 +1,272 @@
+using Spry;
+
+/**
+ * ComponentsTemplateSyntaxPage - Documentation for Spry template syntax
+ * 
+ * This page covers all the special HTML attributes and elements used in
+ * Spry component templates.
+ */
+public class ComponentsTemplateSyntaxPage : PageComponent {
+    
+    public const string ROUTE = "/components/template-syntax";
+    
+    public override string markup { get {
+        return """
+        <div sid="page" class="doc-content">
+            <h1>Template Syntax</h1>
+            
+            <section class="doc-section">
+                <p>
+                    Spry extends HTML with special attributes that enable reactive behavior,
+                    component composition, and dynamic content. This page covers all the
+                    template syntax available in Spry components.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Special Attributes</h2>
+                
+                <h3><code>sid</code> - Spry ID</h3>
+                <p>
+                    The <code>sid</code> attribute assigns a unique identifier to an element within
+                    a component. Use it to select and manipulate elements in your Vala code.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="sid-html"/>
+                <spry-component name="CodeBlockComponent" sid="sid-vala"/>
+            </section>
+            
+            <section class="doc-section">
+                <h3><code>spry-action</code> - HTMX Action Binding</h3>
+                <p>
+                    The <code>spry-action</code> attribute binds an element to trigger a component action
+                    when interacted with. The action is sent to the server via HTMX.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="action-example"/>
+                
+                <p>
+                    See the <a href="/components/actions">Actions</a> page for more details.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h3><code>spry-target</code> - Scoped Targeting</h3>
+                <p>
+                    The <code>spry-target</code> attribute specifies which element should be replaced
+                    when an action completes. It targets elements by their <code>sid</code> within the
+                    same component.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="target-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h3><code>spry-global</code> - Out-of-Band Swaps</h3>
+                <p>
+                    The <code>spry-global</code> attribute marks an element for out-of-band updates.
+                    When included in a response, HTMX will swap this element anywhere on the page
+                    that has a matching <code>id</code>.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="global-html"/>
+                <spry-component name="CodeBlockComponent" sid="global-vala"/>
+            </section>
+            
+            <section class="doc-section">
+                <h3><code>*-expr</code> - Expression Attributes</h3>
+                <p>
+                    Expression attributes dynamically set HTML attributes based on component properties.
+                    Use <code>content-expr</code> for text content, <code>class-*-expr</code> for
+                    conditional classes, and <code>any-attr-expr</code> for any other attribute.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="expr-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h3><code>spry-if</code> / <code>spry-else-if</code> / <code>spry-else</code> - Conditionals</h3>
+                <p>
+                    Use these attributes for conditional rendering. Only elements with truthy
+                    conditions will be included in the output.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="conditional-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h3><code>spry-per-*</code> - Loop Rendering</h3>
+                <p>
+                    Use <code>spry-per-{varname}="expression"</code> to iterate over collections.
+                    The variable name becomes available in nested expressions.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="loop-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Special Elements</h2>
+                
+                <h3><code>&lt;spry-outlet&gt;</code> - Child Component Outlets</h3>
+                <p>
+                    Outlets are placeholders where child components can be inserted dynamically.
+                    Use <code>set_outlet_children()</code> to populate outlets.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="outlet-example"/>
+                
+                <p>
+                    See the <a href="/components/outlets">Outlets</a> page for more details.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h3><code>&lt;spry-component&gt;</code> - Declarative Child Components</h3>
+                <p>
+                    Use <code>&lt;spry-component&gt;</code> to declare child components directly in
+                    your template. Access them with <code>get_component_child&lt;T&gt;()</code>.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="component-html"/>
+                <spry-component name="CodeBlockComponent" sid="component-vala"/>
+            </section>
+            
+            <section class="doc-section">
+                <h3><code>spry-res</code> - Static Resources</h3>
+                <p>
+                    Use <code>spry-res</code> to reference Spry's built-in static resources.
+                    This resolves to <code>/_spry/res/{resource-name}</code> automatically.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="res-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/components/actions" class="nav-card">
+                        <h3>Actions →</h3>
+                        <p>Handle user interactions with actions</p>
+                    </a>
+                    <a href="/components/outlets" class="nav-card">
+                        <h3>Outlets →</h3>
+                        <p>Compose components with outlets</p>
+                    </a>
+                    <a href="/components/continuations" class="nav-card">
+                        <h3>Continuations →</h3>
+                        <p>Real-time updates with SSE</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // sid examples
+        var sid_html = get_component_child<CodeBlockComponent>("sid-html");
+        sid_html.language = "HTML";
+        sid_html.code = """<div sid="container">
+    <span sid="title">Hello</span>
+    <button sid="submit-btn">Submit</button>
+</div>""";
+        
+        var sid_vala = get_component_child<CodeBlockComponent>("sid-vala");
+        sid_vala.language = "Vala";
+        sid_vala.code = """// Access elements by sid in prepare() or handle_action()
+this["title"].text_content = "Updated Title";
+this["submit-btn"].set_attribute("disabled", "true");""";
+        
+        // spry-action example
+        var action_example = get_component_child<CodeBlockComponent>("action-example");
+        action_example.language = "HTML";
+        action_example.code = """<!-- Same-component action -->
+<button spry-action=":Toggle">Toggle</button>
+
+<!-- Cross-component action -->
+<button spry-action="ListComponent:Add">Add Item</button>""";
+        
+        // spry-target example
+        var target_example = get_component_child<CodeBlockComponent>("target-example");
+        target_example.language = "HTML";
+        target_example.code = """<div sid="item" class="item">
+    <span sid="title">Item Title</span>
+    <button spry-action=":Delete" spry-target="item">Delete</button>
+</div>""";
+        
+        // spry-global examples
+        var global_html = get_component_child<CodeBlockComponent>("global-html");
+        global_html.language = "HTML";
+        global_html.code = """<!-- In HeaderComponent -->
+<div id="header" spry-global="header">
+    <h1>Todo App</h1>
+    <span sid="count">0 items</span>
+</div>""";
+        
+        var global_vala = get_component_child<CodeBlockComponent>("global-vala");
+        global_vala.language = "Vala";
+        global_vala.code = """// In another component's handle_action()
+add_globals_from(header);  // Includes header in response for OOB swap""";
+        
+        // Expression attributes example
+        var expr_example = get_component_child<CodeBlockComponent>("expr-example");
+        expr_example.language = "HTML";
+        expr_example.code = """<!-- Content expression -->
+<span content-expr='format("%i%%", this.percent)'>0%</span>
+
+<!-- Style expression -->
+<div style-width-expr='format("%i%%", this.percent)'></div>
+
+<!-- Conditional class -->
+<div class-completed-expr="this.is_completed">Task</div>
+
+<!-- Any attribute expression -->
+<input disabled-expr="this.is_readonly">""";
+        
+        // Conditional example
+        var conditional_example = get_component_child<CodeBlockComponent>("conditional-example");
+        conditional_example.language = "HTML";
+        conditional_example.code = """<div spry-if="this.is_admin">Admin Panel</div>
+<div spry-else-if="this.is_moderator">Moderator Panel</div>
+<div spry-else>User Panel</div>""";
+        
+        // Loop example
+        var loop_example = get_component_child<CodeBlockComponent>("loop-example");
+        loop_example.language = "HTML";
+        loop_example.code = """<ul>
+    <li spry-per-task="this.tasks">
+        <span content-expr="task.name"></span>
+        <span content-expr="task.status"></span>
+    </li>
+</ul>""";
+        
+        // Outlet example
+        var outlet_example = get_component_child<CodeBlockComponent>("outlet-example");
+        outlet_example.language = "HTML";
+        outlet_example.code = """<div sid="list">
+    <spry-outlet sid="items"/>
+</div>""";
+        
+        // Component examples
+        var component_html = get_component_child<CodeBlockComponent>("component-html");
+        component_html.language = "HTML";
+        component_html.code = """<div>
+    <spry-component name="HeaderComponent" sid="header"/>
+    <spry-component name="TodoListComponent" sid="todo-list"/>
+    <spry-component name="FooterComponent" sid="footer"/>
+</div>""";
+        
+        var component_vala = get_component_child<CodeBlockComponent>("component-vala");
+        component_vala.language = "Vala";
+        component_vala.code = """public override async void prepare() throws Error {
+    var header = get_component_child<HeaderComponent>("header");
+    header.title = "My App";
+}""";
+        
+        // Static resources example
+        var res_example = get_component_child<CodeBlockComponent>("res-example");
+        res_example.language = "HTML";
+        res_example.code = """<script spry-res="htmx.js"></script>
+<script spry-res="htmx-sse.js"></script>""";
+    }
+}

+ 91 - 163
demo/Pages/HomePage.vala

@@ -1,14 +1,11 @@
-using Astralis;
-using Invercargill;
-using Invercargill.DataStructures;
-using Inversion;
 using Spry;
+using Inversion;
 
 /**
- * HomePage - The main landing page
+ * HomePage - Documentation landing page for the Spry framework
  * 
- * Eye-catching hero section with gradient text,
- * feature highlights, and call-to-action buttons
+ * Clean, minimal design focused on helping developers get started
+ * with Spry quickly.
  */
 public class HomePage : PageComponent {
     
@@ -18,173 +15,104 @@ public class HomePage : PageComponent {
     
     public override string markup { get {
         return """
-        <div sid="home">
+        <div class="doc-content">
+            
             <!-- Hero Section -->
-            <section class="hero">
-                <div class="container">
-                    <span class="hero-badge">
-                        ⚡ Free & Open Source
-                    </span>
-                    <h1>Build Modern Web Apps in Vala</h1>
-                    <p class="hero-subtitle">
-                        Spry is a component-based web framework featuring HTMX integration,
-                        reactive templates, and dependency injection. Fast, type-safe, and elegant.
-                    </p>
-                    <div class="hero-actions">
-                        <a href="/demo" class="btn btn-primary">
-                            ✨ Try Live Demo
-                        </a>
-                        <a href="/features" class="btn btn-secondary">
-                            Explore Features
-                        </a>
-                    </div>
-                </div>
+            <section class="home-hero">
+                <h1 class="hero-title">Spry</h1>
+                <p class="hero-tagline">A component-based web framework for Vala</p>
             </section>
-
-            <!-- Quick Stats -->
-            <section class="section">
-                <div class="container">
-                    <div class="stats-grid">
-                        <div class="stat-card">
-                            <div class="stat-value">100%</div>
-                            <div class="stat-label">Type-Safe Vala</div>
-                        </div>
-                        <div class="stat-card">
-                            <div class="stat-value">0</div>
-                            <div class="stat-label">JavaScript Required</div>
-                        </div>
-                        <div class="stat-card">
-                            <div class="stat-value">∞</div>
-                            <div class="stat-label">Possibilities</div>
-                        </div>
-                        <div class="stat-card">
-                            <div class="stat-value">FREE</div>
-                            <div class="stat-label">Forever</div>
-                        </div>
-                    </div>
-                </div>
+            
+            <!-- Introduction -->
+            <section class="doc-section">
+                <p class="intro">
+                    Spry is a web framework that lets you build dynamic, interactive web applications
+                    using Vala. It features HTMX integration for reactive UIs, dependency injection
+                    for clean architecture, and a component model that keeps markup and logic together.
+                </p>
+                <p>
+                    Whether you're building a simple web service or a complex interactive application,
+                    Spry provides the tools you need with full type safety and native performance.
+                </p>
             </section>
-
-            <!-- Feature Highlights -->
-            <section class="section">
-                <div class="container">
-                    <div class="section-header">
-                        <h2>Why Choose Spry?</h2>
-                        <p>Build powerful web applications with a framework designed for developer happiness</p>
-                    </div>
-                    <div class="features-grid">
-                        <spry-outlet sid="features"/>
-                    </div>
+            
+            <!-- Quick Links -->
+            <section class="doc-section">
+                <h2>Documentation</h2>
+                <div class="quick-links">
+                    <a href="/components/overview" class="quick-link-card">
+                        <h3>Components</h3>
+                        <p>Learn about Spry's component model, lifecycle, and how to build interactive UIs.</p>
+                    </a>
+                    <a href="/page-components/overview" class="quick-link-card">
+                        <h3>Page Components</h3>
+                        <p>Understand how to create pages and use templates for consistent layouts.</p>
+                    </a>
+                    <a href="/static-resources/overview" class="quick-link-card">
+                        <h3>Static Resources</h3>
+                        <p>Serve CSS, JavaScript, images, and other static assets efficiently.</p>
+                    </a>
                 </div>
             </section>
-
-            <!-- Code Example -->
-            <section class="section">
-                <div class="container">
-                    <div class="section-header">
-                        <h2>Clean, Intuitive Code</h2>
-                        <p>Write components that are easy to understand and maintain</p>
-                    </div>
-                    <div class="code-block">
-                        <div class="code-header">
-                            <div class="code-dots">
-                                <div class="code-dot red"></div>
-                                <div class="code-dot yellow"></div>
-                                <div class="code-dot green"></div>
-                            </div>
-                            <span class="code-lang">Vala</span>
-                        </div>
-                        <pre><code>class CounterComponent : Component {
-    private int count = 0;
-
-    public override string markup {
-        owned get {
-            // Use spry-action for interactivity
-            return @"Counter: $(count)
-                [Button: spry-action=':Increment' +]
-                [Button: spry-action=':Decrement' -]";
-        }
-    }
-
-    public async override void handle_action(string action) {
-        if (action == "Increment") count++;
-        else if (action == "Decrement") count--;
-    }
-}</code></pre>
-                    </div>
-                </div>
+            
+            <!-- Get Started Code Example -->
+            <section class="doc-section">
+                <h2>Get Started</h2>
+                <p>
+                    Here's a minimal "Hello World" Spry application to get you started:
+                </p>
+                <spry-component name="CodeBlockComponent" sid="hello-code"/>
             </section>
-
-            <!-- CTA Section -->
-            <section class="section">
-                <div class="container">
-                    <div class="freedom-section">
-                        <div class="freedom-icon">🕊</div>
-                        <h2>Free as in Freedom</h2>
-                        <p>
-                            Spry is Free/Libre Open Source Software. Use it, study it, modify it,
-                            and share it freely. Built by the community, for the community.
-                        </p>
-                        <div class="hero-actions">
-                            <a href="/freedom" class="btn btn-accent">
-                                Learn About Freedom
-                            </a>
-                            <a href="/ecosystem" class="btn btn-secondary">
-                                Explore Ecosystem
-                            </a>
-                        </div>
-                    </div>
-                </div>
+            
+            <!-- Features -->
+            <section class="doc-section">
+                <h2>Why Spry?</h2>
+                <ul class="features-list">
+                    <li>
+                        <strong>Type-Safe</strong> — Full Vala type safety catches errors at compile time,
+                        not runtime.
+                    </li>
+                    <li>
+                        <strong>Component-Based</strong> — Build encapsulated, reusable components with
+                        markup and logic in one place.
+                    </li>
+                    <li>
+                        <strong>HTMX Integration</strong> — Create dynamic interfaces without writing
+                        JavaScript. HTMX handles the complexity.
+                    </li>
+                    <li>
+                        <strong>Dependency Injection</strong> — Clean architecture with Inversion of
+                        Control for services and stores.
+                    </li>
+                    <li>
+                        <strong>Native Performance</strong> — Compiled to native code for maximum
+                        throughput and minimal resource usage.
+                    </li>
+                    <li>
+                        <strong>Free & Open Source</strong> — Released under a permissive license,
+                        free to use for any project.
+                    </li>
+                </ul>
             </section>
+            
         </div>
         """;
     }}
     
     public override async void prepare() throws Error {
-        var features = new Series<Renderable>();
-        
-        var feature1 = factory.create<FeatureCardComponent>();
-        feature1.icon = "purple";
-        feature1.icon_emoji = "⚡";
-        feature1.title = "HTMX Integration";
-        feature1.description = "Build dynamic UIs without writing JavaScript. HTMX handles the complexity, you handle the logic.";
-        features.add(feature1);
-        
-        var feature2 = factory.create<FeatureCardComponent>();
-        feature2.icon = "blue";
-        feature2.icon_emoji = "🔧";
-        feature2.title = "Dependency Injection";
-        feature2.description = "Clean architecture with Inversion of Control. Inject services, stores, and components effortlessly.";
-        features.add(feature2);
-        
-        var feature3 = factory.create<FeatureCardComponent>();
-        feature3.icon = "green";
-        feature3.icon_emoji = "🎨";
-        feature3.title = "Reactive Templates";
-        feature3.description = "Declarative templates with outlets and automatic updates. Your UI stays in sync with your data.";
-        features.add(feature3);
-        
-        var feature4 = factory.create<FeatureCardComponent>();
-        feature4.icon = "purple";
-        feature4.icon_emoji = "🔒";
-        feature4.title = "Type-Safe";
-        feature4.description = "Full Vala type safety means fewer runtime errors. The compiler catches bugs before you do.";
-        features.add(feature4);
-        
-        var feature5 = factory.create<FeatureCardComponent>();
-        feature5.icon = "blue";
-        feature5.icon_emoji = "🚀";
-        feature5.title = "High Performance";
-        feature5.description = "Built on Astralis for maximum throughput. Native code performance with web framework convenience.";
-        features.add(feature5);
-        
-        var feature6 = factory.create<FeatureCardComponent>();
-        feature6.icon = "green";
-        feature6.icon_emoji = "📦";
-        feature6.title = "Modular Design";
-        feature6.description = "Use what you need, extend what you want. Clean separation of concerns at every level.";
-        features.add(feature6);
-        
-        set_outlet_children("features", features);
+        var helloCode = get_component_child<CodeBlockComponent>("hello-code");
+        helloCode.language = "Vala";
+        helloCode.code = "public class HelloPage : PageComponent {\n" +
+        "    public override string markup { get {\n" +
+        "        return \"\"\"<!DOCTYPE html>\n" +
+        "<html>\n" +
+        "<body>\n" +
+        "    <h1>Hello, Spry!</h1>\n" +
+        "</body>\n" +
+        "</html>\"\"\";\n" +
+        "    }}\n" +
+        "}\n\n" +
+        "// In Main.vala\n" +
+        "spry_cfg.add_page<HelloPage>(new EndpointRoute(\"/\"));";
     }
 }

+ 267 - 0
demo/Pages/PageComponentsOverviewPage.vala

@@ -0,0 +1,267 @@
+using Spry;
+using Inversion;
+
+/**
+ * PageComponentsOverviewPage - Introduction to PageComponent
+ * 
+ * This page explains what PageComponents are, how they differ from regular
+ * Components, and when to use them.
+ */
+public class PageComponentsOverviewPage : PageComponent {
+    
+    public const string ROUTE = "/page-components/overview";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div class="doc-content">
+            <div class="doc-hero">
+                <h1>Page Components</h1>
+                <p class="doc-subtitle">Combine Component and Endpoint into a single powerful abstraction</p>
+            </div>
+            
+            <section class="doc-section">
+                <h2>What are Page Components?</h2>
+                <p>
+                    A <code>PageComponent</code> is a special type of component that acts as both a 
+                    <strong>Component</strong> AND an <strong>Endpoint</strong>. This means it can render
+                    HTML templates <em>and</em> handle HTTP requests at a specific route.
+                </p>
+                <p>
+                    In traditional web frameworks, you often need separate classes for routes/endpoints
+                    and for rendering views. PageComponent eliminates this duplication by combining
+                    both responsibilities into a single cohesive class.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>PageComponent vs Component</h2>
+                <p>
+                    Understanding when to use <code>PageComponent</code> vs <code>Component</code> is essential:
+                </p>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Feature</th>
+                            <th>Component</th>
+                            <th>PageComponent</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td>Has markup</td>
+                            <td>✓ Yes</td>
+                            <td>✓ Yes</td>
+                        </tr>
+                        <tr>
+                            <td>Handles HTTP routes</td>
+                            <td>✗ No</td>
+                            <td>✓ Yes</td>
+                        </tr>
+                        <tr>
+                            <td>Can be nested in outlets</td>
+                            <td>✓ Yes</td>
+                            <td>✗ No (top-level only)</td>
+                        </tr>
+                        <tr>
+                            <td>Implements Endpoint</td>
+                            <td>✗ No</td>
+                            <td>✓ Yes</td>
+                        </tr>
+                        <tr>
+                            <td>Wrapped by templates</td>
+                            <td>✗ No</td>
+                            <td>✓ Yes</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </section>
+            
+            <section class="doc-section">
+                <h2>When to Use Each</h2>
+                
+                <div class="feature-grid">
+                    <div class="feature-card">
+                        <h3>Use PageComponent when:</h3>
+                        <ul>
+                            <li>Creating top-level pages (routes)</li>
+                            <li>Building documentation pages</li>
+                            <li>Rendering full HTML documents</li>
+                            <li>The component needs its own URL</li>
+                        </ul>
+                    </div>
+                    <div class="feature-card">
+                        <h3>Use Component when:</h3>
+                        <ul>
+                            <li>Building reusable UI elements</li>
+                            <li>Creating widgets or cards</li>
+                            <li>Nesting inside other components</li>
+                            <li>Rendering partial content</li>
+                        </ul>
+                    </div>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Basic PageComponent Example</h2>
+                <p>
+                    Here's how to create a simple PageComponent:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="basic-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Route Registration</h2>
+                <p>
+                    PageComponents are registered as endpoints in your application's Main.vala.
+                    You register both the component type and its route:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="route-registration"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Dependency Injection in Pages</h2>
+                <p>
+                    PageComponents support the same dependency injection patterns as regular components.
+                    Use <code>inject<T>()</code> to access services, stores, and factories:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="di-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Template Wrapping</h2>
+                <p>
+                    One of the most powerful features of PageComponents is automatic template wrapping.
+                    When a PageComponent renders, it is automatically wrapped by matching 
+                    <code>PageTemplate</code> instances.
+                </p>
+                <p>
+                    This means your PageComponent markup only needs to contain the <em>content</em>
+                    of the page, not the full HTML document structure. The template provides the
+                    <code><html></code>, <code><head></code>, <code><body></code>,
+                    navigation, and footer.
+                </p>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 Tip:</strong> Learn more about templates in the 
+                        <a href="/page-components/templates">Page Templates</a> documentation.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>How handle_request() Works</h2>
+                <p>
+                    The <code>PageComponent</code> base class implements the <code>Endpoint</code>
+                    interface's <code>handle_request()</code> method. This method:
+                </p>
+                
+                <ol class="doc-list">
+                    <li>Collects all matching <code>PageTemplate</code> instances sorted by prefix depth</li>
+                    <li>Renders each template in order</li>
+                    <li>Finds <code>&lt;spry-template-outlet/&gt;</code> elements in each template</li>
+                    <li>Nests the content into the outlet, building the complete document</li>
+                    <li>Returns the final HTML as an <code>HttpResult</code></li>
+                </ol>
+                
+                <p>
+                    You typically don't need to override <code>handle_request()</code> - the default
+                    implementation handles everything for you.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/page-components/templates" class="nav-card">
+                        <h3>Page Templates →</h3>
+                        <p>Learn how to create layout templates that wrap your pages</p>
+                    </a>
+                    <a href="/components/overview" class="nav-card">
+                        <h3>Components Overview →</h3>
+                        <p>Review the basics of Spry components</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Basic PageComponent example
+        var basic_example = get_component_child<CodeBlockComponent>("basic-example");
+        basic_example.language = "vala";
+        basic_example.code = "using Spry;\n" +
+            "using Inversion;\n\n" +
+            "public class DocumentationPage : PageComponent {\n" +
+            "    \n" +
+            "    // Dependency injection works the same as Component\n" +
+            "    private ComponentFactory factory = inject<ComponentFactory>();\n" +
+            "    \n" +
+            "    public override string markup { get {\n" +
+            "        return \"\"\"\n" +
+            "        <div class=\"page-content\">\n" +
+            "            <h1>My Documentation Page</h1>\n" +
+            "            <p>This page handles its own route.</p>\n" +
+            "        </div>\n" +
+            "        \"\"\";\n" +
+            "    }}\n" +
+            "    \n" +
+            "    public override async void prepare() throws Error {\n" +
+            "        // Set up page content\n" +
+            "    }\n" +
+            "}";
+        
+        // Route registration example
+        var route_reg = get_component_child<CodeBlockComponent>("route-registration");
+        route_reg.language = "vala";
+        route_reg.code = "// In Main.vala:\n\n" +
+            "// Register the page component with the dependency container\n" +
+            "application.add_transient<DocumentationPage>();\n\n" +
+            "// Register the page as an endpoint at a specific route\n" +
+            "application.add_endpoint<DocumentationPage>(\n" +
+            "    new EndpointRoute(\"/docs/page\")\n" +
+            ");\n\n" +
+            "// Now visiting /docs/page will render DocumentationPage\n" +
+            "// automatically wrapped by any matching PageTemplate";
+        
+        // Dependency injection example
+        var di_example = get_component_child<CodeBlockComponent>("di-example");
+        di_example.language = "vala";
+        di_example.code = "public class UserProfilePage : PageComponent {\n" +
+            "    \n" +
+            "    // Inject services you need\n" +
+            "    private ComponentFactory factory = inject<ComponentFactory>();\n" +
+            "    private UserStore user_store = inject<UserStore>();\n" +
+            "    private HttpContext http_context = inject<HttpContext>();\n" +
+            "    \n" +
+            "    public override string markup { get {\n" +
+            "        return \"\"\"\n" +
+            "        <div class=\"profile-page\">\n" +
+            "            <h2 sid=\"username\"></h2>\n" +
+            "            <p sid=\"email\"></p>\n" +
+            "        </div>\n" +
+            "        \"\"\";\n" +
+            "    }}\n" +
+            "    \n" +
+            "    public override async void prepare() throws Error {\n" +
+            "        // Access route parameters from http_context\n" +
+            "        var user_id = http_context.request.query_params\n" +
+            "            .get_any_or_default(\"id\", \"guest\");\n" +
+            "        \n" +
+            "        // Use injected store to fetch data\n" +
+            "        var user = yield user_store.get_user(user_id);\n" +
+            "        \n" +
+            "        // Populate template\n" +
+            "        this[\"username\"].text_content = user.name;\n" +
+            "        this[\"email\"].text_content = user.email;\n" +
+            "    }\n" +
+            "}";
+    }
+}

+ 419 - 0
demo/Pages/PageTemplatesPage.vala

@@ -0,0 +1,419 @@
+using Spry;
+using Inversion;
+
+/**
+ * PageTemplatesPage - Documentation for Page Templates
+ * 
+ * This page explains how PageTemplates work, how to create them,
+ * and how they wrap PageComponents to provide site-wide layouts.
+ */
+public class PageTemplatesPage : PageComponent {
+    
+    public const string ROUTE = "/page-components/templates";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div class="doc-content">
+            <div class="doc-hero">
+                <h1>Page Templates</h1>
+                <p class="doc-subtitle">Create reusable layouts that wrap your page components</p>
+            </div>
+            
+            <section class="doc-section">
+                <h2>What are Page Templates?</h2>
+                <p>
+                    A <code>PageTemplate</code> is a special component that provides the outer HTML
+                    structure for your pages. Templates define the <code><html></code>,
+                    <code><head></code>, <code><body></code>, and common elements like
+                    navigation headers and footers.
+                </p>
+                <p>
+                    The key feature of a PageTemplate is the <code>&lt;spry-template-outlet/&gt;</code>
+                    element, which marks where page content will be inserted. When a PageComponent
+                    renders, it automatically gets nested inside matching templates.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The Template Outlet</h2>
+                <p>
+                    The <code>&lt;spry-template-outlet/&gt;</code> element is the placeholder where
+                    page content gets inserted. Every PageTemplate must include at least one outlet.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="outlet-example"/>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 How it works:</strong> When rendering, Spry finds all templates
+                        matching the current route, renders them in order of specificity, and nests
+                        each one's content into the previous template's outlet.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Creating a MainTemplate</h2>
+                <p>
+                    Here's a complete example of a site-wide template that provides the HTML
+                    document structure, head elements, navigation, and footer:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="main-template"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Template Registration</h2>
+                <p>
+                    Templates are registered with a <strong>prefix</strong> that determines which
+                    routes they apply to. The prefix is passed via metadata during registration:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="template-registration"/>
+                
+                <p>
+                    In this example, the <code>TemplateRoutePrefix("")</code> with an empty string
+                    matches <strong>all routes</strong>, making it the default site-wide template.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Route Prefix Matching</h2>
+                <p>
+                    Templates can target specific sections of your site using route prefixes.
+                    The prefix determines which routes the template will wrap:
+                </p>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Prefix</th>
+                            <th>Matches Routes</th>
+                            <th>Use Case</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td><code>""</code> (empty)</td>
+                            <td>All routes</td>
+                            <td>Site-wide default template</td>
+                        </tr>
+                        <tr>
+                            <td><code>"/admin"</code></td>
+                            <td>/admin/*, /admin/settings, etc.</td>
+                            <td>Admin section layout</td>
+                        </tr>
+                        <tr>
+                            <td><code>"/docs"</code></td>
+                            <td>/docs/*, /docs/getting-started, etc.</td>
+                            <td>Documentation section</td>
+                        </tr>
+                        <tr>
+                            <td><code>"/api"</code></td>
+                            <td>/api/* routes</td>
+                            <td>API documentation or explorer</td>
+                        </tr>
+                    </tbody>
+                </table>
+                
+                <h3>How Matching Works</h3>
+                <p>
+                    The <code>TemplateRoutePrefix.matches_route()</code> method compares the template's
+                    prefix segments against the route segments. A template matches if its prefix
+                    segments are a prefix of the route segments.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="matching-logic"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Multiple Templates</h2>
+                <p>
+                    You can have multiple templates for different sections of your site.
+                    Templates are sorted by <strong>rank</strong> (prefix depth) and applied
+                    from lowest to highest rank:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="multiple-templates"/>
+                
+                <h3>Template Nesting Order</h3>
+                <p>
+                    For a route like <code>/admin/users</code>, templates would be applied in order:
+                </p>
+                
+                <ol class="doc-list">
+                    <li><strong>MainTemplate</strong> (prefix: "") - rank 0</li>
+                    <li><strong>AdminTemplate</strong> (prefix: "/admin") - rank 1</li>
+                    <li><strong>PageComponent</strong> - The actual page content</li>
+                </ol>
+                
+                <p>
+                    Each template's outlet receives the content from the next item in the chain,
+                    creating nested layouts.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Section-Specific Template Example</h2>
+                <p>
+                    Here's an example of a template specifically for the admin section that
+                    adds an admin sidebar:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="admin-template"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Head Content Merging</h2>
+                <p>
+                    When templates wrap pages, the <code><head></code> elements are automatically
+                    merged. If a PageComponent or nested template has <code><head></code> content,
+                    those elements are appended to the outer template's head.
+                </p>
+                
+                <p>
+                    This allows pages to add their own stylesheets, scripts, or meta tags while
+                    still benefiting from the template's common head elements.
+                </p>
+                
+                <div class="warning-box">
+                    <p>
+                        <strong>⚠️ Note:</strong> Head merging only works when templates render
+                        actual <code><head></code> elements. Make sure your templates include
+                        a proper HTML structure with head and body sections.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Template vs Component</h2>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Feature</th>
+                            <th>PageTemplate</th>
+                            <th>Component</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td>Base class</td>
+                            <td><code>Component</code></td>
+                            <td><code>Component</code></td>
+                        </tr>
+                        <tr>
+                            <td>Has markup</td>
+                            <td>✓ Yes</td>
+                            <td>✓ Yes</td>
+                        </tr>
+                        <tr>
+                            <td>Contains outlet</td>
+                            <td>✓ Required</td>
+                            <td>✗ Optional</td>
+                        </tr>
+                        <tr>
+                            <td>Route matching</td>
+                            <td>By prefix</td>
+                            <td>N/A</td>
+                        </tr>
+                        <tr>
+                            <td>Wraps pages</td>
+                            <td>✓ Yes</td>
+                            <td>✗ No</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Best Practices</h2>
+                
+                <ul class="doc-list">
+                    <li><strong>Keep templates focused:</strong> Each template should handle one level of layout (site-wide, section-specific)</li>
+                    <li><strong>Use semantic HTML:</strong> Include proper <code><header></code>, <code><main></code>, <code><footer></code> elements</li>
+                    <li><strong>Include common resources:</strong> Add shared stylesheets and scripts in your main template</li>
+                    <li><strong>Plan your prefix hierarchy:</strong> Design your URL structure to work with template prefixes</li>
+                    <li><strong>Don't duplicate content:</strong> Let templates handle repeated elements like navigation</li>
+                </ul>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/page-components/overview" class="nav-card">
+                        <h3>← Page Components</h3>
+                        <p>Learn about PageComponent basics</p>
+                    </a>
+                    <a href="/components/outlets" class="nav-card">
+                        <h3>Component Outlets →</h3>
+                        <p>Use outlets for nested components</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        // Outlet example
+        var outlet_example = get_component_child<CodeBlockComponent>("outlet-example");
+        outlet_example.language = "xml";
+        outlet_example.code = "<!DOCTYPE html>\n" +
+            "<html lang=\"en\">\n" +
+            "<head>\n" +
+            "    <title>My Site</title>\n" +
+            "    <link rel=\"stylesheet\" href=\"/styles/main.css\">\n" +
+            "</head>\n" +
+            "<body>\n" +
+            "    <header>\n" +
+            "        <nav><!-- Site navigation --></nav>\n" +
+            "    </header>\n" +
+            "    \n" +
+            "    <main>\n" +
+            "        <!-- Page content is inserted here -->\n" +
+            "        <spry-template-outlet />\n" +
+            "    </main>\n" +
+            "    \n" +
+            "    <footer>\n" +
+            "        <p>&copy; 2024 My Site</p>\n" +
+            "    </footer>\n" +
+            "</body>\n" +
+            "</html>";
+        
+        // Main template example
+        var main_template = get_component_child<CodeBlockComponent>("main-template");
+        main_template.language = "vala";
+        main_template.code = "using Spry;\n\n" +
+            "/**\n" +
+            " * MainTemplate - Site-wide layout template\n" +
+            " * \n" +
+            " * Wraps all pages with common HTML structure,\n" +
+            " * navigation, and footer.\n" +
+            " */\n" +
+            "public class MainTemplate : PageTemplate {\n" +
+            "    \n" +
+            "    public override string markup { get {\n" +
+            "        return \"\"\"\n" +
+            "        <!DOCTYPE html>\n" +
+            "        <html lang=\"en\">\n" +
+            "        <head>\n" +
+            "            <meta charset=\"UTF-8\">\n" +
+            "            <meta name=\"viewport\" \n" +
+            "                  content=\"width=device-width, initial-scale=1.0\">\n" +
+            "            <title>My Spry Application</title>\n" +
+            "            <link rel=\"stylesheet\" href=\"/styles/main.css\">\n" +
+            "            <script spry-res=\"htmx.js\"></script>\n" +
+            "        </head>\n" +
+            "        <body>\n" +
+            "            <header class=\"site-header\">\n" +
+            "                <nav>\n" +
+            "                    <a href=\"/\" class=\"logo\">My App</a>\n" +
+            "                    <ul>\n" +
+            "                        <li><a href=\"/docs\">Docs</a></li>\n" +
+            "                        <li><a href=\"/about\">About</a></li>\n" +
+            "                    </ul>\n" +
+            "                </nav>\n" +
+            "            </header>\n" +
+            "            \n" +
+            "            <main>\n" +
+            "                <spry-template-outlet />\n" +
+            "            </main>\n" +
+            "            \n" +
+            "            <footer class=\"site-footer\">\n" +
+            "                <p>&copy; 2024 My App. Built with Spry.</p>\n" +
+            "            </footer>\n" +
+            "        </body>\n" +
+            "        </html>\n" +
+            "        \"\"\";\n" +
+            "    }}\n" +
+            "}";
+        
+        // Template registration
+        var template_reg = get_component_child<CodeBlockComponent>("template-registration");
+        template_reg.language = "vala";
+        template_reg.code = "// In Main.vala:\n\n" +
+            "var spry_cfg = application.configure_with<SpryConfigurator>();\n\n" +
+            "// Register template with empty prefix (matches all routes)\n" +
+            "spry_cfg.add_template<MainTemplate>(\"\");\n\n" +
+            "// The TemplateRoutePrefix is created internally from the string\n" +
+            "// and used for route matching";
+        
+        // Matching logic
+        var matching_logic = get_component_child<CodeBlockComponent>("matching-logic");
+        matching_logic.language = "vala";
+        matching_logic.code = "// TemplateRoutePrefix matching logic (simplified)\n\n" +
+            "public class TemplateRoutePrefix : Object {\n" +
+            "    public uint rank { get; private set; }\n" +
+            "    public string prefix { get; private set; }\n" +
+            "    \n" +
+            "    public TemplateRoutePrefix(string prefix) {\n" +
+            "        this.prefix = prefix;\n" +
+            "        // Rank is the number of path segments\n" +
+            "        rank = prefix.split(\"/\").length - 1;\n" +
+            "    }\n" +
+            "    \n" +
+            "    public bool matches_route(RouteContext context) {\n" +
+            "        // Returns true if prefix segments match\n" +
+            "        // the beginning of the route segments\n" +
+            "        return context.matched_route.route_segments\n" +
+            "            .starts_with(this.prefix_segments);\n" +
+            "    }\n" +
+            "}\n\n" +
+            "// Example: prefix \"/admin\" matches:\n" +
+            "//   /admin          ✓ (exact match)\n" +
+            "//   /admin/users    ✓ (prefix match)\n" +
+            "//   /admin/settings ✓ (prefix match)\n" +
+            "//   /user/admin     ✗ (not a prefix match)";
+        
+        // Multiple templates
+        var multiple_templates = get_component_child<CodeBlockComponent>("multiple-templates");
+        multiple_templates.language = "vala";
+        multiple_templates.code = "// In Main.vala:\n\n" +
+            "var spry_cfg = application.configure_with<SpryConfigurator>();\n\n" +
+            "// Site-wide template (rank 0, matches everything)\n" +
+            "spry_cfg.add_template<MainTemplate>(\"\");\n\n" +
+            "// Admin section template (rank 1, matches /admin/*)\n" +
+            "spry_cfg.add_template<AdminTemplate>(\"/admin\");\n\n" +
+            "// Documentation template (rank 1, matches /docs/*)\n" +
+            "spry_cfg.add_template<DocsTemplate>(\"/docs\");\n\n" +
+            "// API docs template (rank 2, matches /docs/api/*)\n" +
+            "spry_cfg.add_template<ApiDocsTemplate>(\"/docs/api\");";
+        
+        // Admin template example
+        var admin_template = get_component_child<CodeBlockComponent>("admin-template");
+        admin_template.language = "vala";
+        admin_template.code = "using Spry;\n\n" +
+            "/**\n" +
+            " * AdminTemplate - Layout for admin section\n" +
+            " * \n" +
+            " * Adds an admin sidebar to all /admin/* pages.\n" +
+            " */\n" +
+            "public class AdminTemplate : PageTemplate {\n" +
+            "    \n" +
+            "    public override string markup { get {\n" +
+            "        return \"\"\"\n" +
+            "        <div class=\"admin-layout\">\n" +
+            "            <aside class=\"admin-sidebar\">\n" +
+            "                <h3>Admin</h3>\n" +
+            "                <nav>\n" +
+            "                    <a href=\"/admin\">Dashboard</a>\n" +
+            "                    <a href=\"/admin/users\">Users</a>\n" +
+            "                    <a href=\"/admin/settings\">Settings</a>\n" +
+            "                </nav>\n" +
+            "            </aside>\n" +
+            "            \n" +
+            "            <div class=\"admin-content\">\n" +
+            "                <!-- Page content inserted here -->\n" +
+            "                <spry-template-outlet />\n" +
+            "            </div>\n" +
+            "        </div>\n" +
+            "        \"\"\";\n" +
+            "    }}\n" +
+            "}\n\n" +
+            "// Register with /admin prefix\n" +
+            "// spry_cfg.add_template<AdminTemplate>(\"/admin\");";
+    }
+}

+ 414 - 0
demo/Pages/StaticResourcesMkssrPage.vala

@@ -0,0 +1,414 @@
+using Spry;
+using Inversion;
+
+/**
+ * StaticResourcesMkssrPage - Documentation for the spry-mkssr tool
+ * 
+ * This page explains how to use spry-mkssr to generate static resources
+ * for Spry applications.
+ */
+public class StaticResourcesMkssrPage : PageComponent {
+    
+    public const string ROUTE = "/static-resources/spry-mkssr";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div sid="page" class="doc-content">
+            <h1>spry-mkssr Tool</h1>
+            
+            <section class="doc-section">
+                <h2>What is spry-mkssr?</h2>
+                <p>
+                    <code>spry-mkssr</code> is a command-line tool that generates Spry Static Resource
+                    files from any input file. It handles compression, hashing, and metadata generation
+                    so your web application can serve optimized static content efficiently.
+                </p>
+                <p>
+                    The tool can generate two types of output:
+                </p>
+                <ul>
+                    <li><strong>SSR files</strong> (<code>.ssr</code>) - Pre-compiled resource files read at runtime</li>
+                    <li><strong>Vala source files</strong> - Embedded resources compiled into your binary</li>
+                </ul>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Command-Line Usage</h2>
+                
+                <spry-component name="CodeBlockComponent" sid="usage-code"/>
+                
+                <h3>Options</h3>
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Option</th>
+                            <th>Description</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td><code>-o, --output=FILE</code></td>
+                            <td>Output file name (default: input.ssr or ClassNameResource.vala)</td>
+                        </tr>
+                        <tr>
+                            <td><code>-c, --content-type=TYPE</code></td>
+                            <td>Override content type (e.g., text/css, application/javascript)</td>
+                        </tr>
+                        <tr>
+                            <td><code>-n, --name=NAME</code></td>
+                            <td>Override resource name (default: input filename)</td>
+                        </tr>
+                        <tr>
+                            <td><code>--vala</code></td>
+                            <td>Generate Vala source file instead of SSR</td>
+                        </tr>
+                        <tr>
+                            <td><code>--ns=NAMESPACE</code></td>
+                            <td>Namespace for generated Vala class (requires --vala)</td>
+                        </tr>
+                        <tr>
+                            <td><code>-v, --version</code></td>
+                            <td>Show version information</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Generating SSR Files</h2>
+                <p>
+                    By default, spry-mkssr generates <code>.ssr</code> files that can be read
+                    at runtime by <code>FileStaticResource</code>:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="ssr-example"/>
+                
+                <p>
+                    The generated SSR file contains:
+                </p>
+                <ul>
+                    <li><strong>Magic header</strong> - "spry-sr\0" identifier</li>
+                    <li><strong>Resource name</strong> - Used in URLs</li>
+                    <li><strong>Content type</strong> - MIME type for HTTP headers</li>
+                    <li><strong>SHA-512 hash</strong> - For ETag generation</li>
+                    <li><strong>Encoding headers</strong> - Offset and size for each encoding</li>
+                    <li><strong>Compressed data</strong> - gzip, zstd, brotli, and identity</li>
+                </ul>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Generating Vala Source Files</h2>
+                <p>
+                    Use the <code>--vala</code> flag to generate a Vala source file that embeds
+                    the resource directly into your binary:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="vala-example"/>
+                
+                <p>
+                    This creates a class that extends <code>ConstantStaticResource</code> with
+                    the resource data embedded as <code>const uint8[]</code> arrays.
+                </p>
+                
+                <h3>Namespace Configuration</h3>
+                <p>
+                    Use <code>--ns</code> to place the generated class in a namespace:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="namespace-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Generated Vala Class Structure</h2>
+                <p>
+                    The generated Vala class follows this structure:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="class-structure"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Integrating with Meson</h2>
+                <p>
+                    The recommended way to use spry-mkssr is with meson's 
+                    <code>custom_target</code> to generate resources at build time:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="meson-example"/>
+                
+                <h3>Complete meson.build Example</h3>
+                <spry-component name="CodeBlockComponent" sid="meson-complete"/>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 Tip:</strong> The <code>@INPUT@</code> and <code>@OUTPUT@</code>
+                        placeholders are automatically replaced by meson with the actual file paths.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Compression Details</h2>
+                <p>
+                    spry-mkssr compresses resources with the highest compression levels:
+                </p>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Encoding</th>
+                            <th>Level</th>
+                            <th>Best For</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td>gzip</td>
+                            <td>9 (maximum)</td>
+                            <td>Universal compatibility</td>
+                        </tr>
+                        <tr>
+                            <td>zstd</td>
+                            <td>19 (maximum)</td>
+                            <td>Modern browsers, fast decompression</td>
+                        </tr>
+                        <tr>
+                            <td>brotli</td>
+                            <td>11 (maximum)</td>
+                            <td>Best compression for text assets</td>
+                        </tr>
+                        <tr>
+                            <td>identity</td>
+                            <td>N/A</td>
+                            <td>No compression (always included)</td>
+                        </tr>
+                    </tbody>
+                </table>
+                
+                <p>
+                    Encodings are only included if they result in smaller file sizes than
+                    the original. The tool reports compression statistics during generation:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="compression-output"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Content Type Detection</h2>
+                <p>
+                    spry-mkssr automatically detects content types based on file extensions
+                    using GLib's content type guessing. You can override this with the
+                    <code>-c</code> flag:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="content-type-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Resource Naming</h2>
+                <p>
+                    By default, the resource name is derived from the input filename. For
+                    Vala source generation, the class name is created by converting the
+                    resource name to PascalCase and appending "Resource":
+                </p>
+                
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Input File</th>
+                            <th>Resource Name</th>
+                            <th>Generated Class</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td>docs.css</td>
+                            <td>docs.css</td>
+                            <td>DocsResource.vala → DocsResource</td>
+                        </tr>
+                        <tr>
+                            <td>htmx.min.js</td>
+                            <td>htmx.min.js</td>
+                            <td>HtmxMinResource.vala → HtmxMinResource</td>
+                        </tr>
+                        <tr>
+                            <td>-n htmx.js htmx.min.js</td>
+                            <td>htmx.js</td>
+                            <td>HtmxResource.vala → HtmxResource</td>
+                        </tr>
+                    </tbody>
+                </table>
+                
+                <p>
+                    Use the <code>-n</code> flag to set a specific resource name:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="naming-example"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/static-resources/overview" class="nav-card">
+                        <h3>← Static Resources Overview</h3>
+                        <p>Back to static resources introduction</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        var usage_code = get_component_child<CodeBlockComponent>("usage-code");
+        usage_code.language = "Bash";
+        usage_code.code = "spry-mkssr [OPTION?] INPUT_FILE\n\n" +
+            "# Examples:\n" +
+            "spry-mkssr styles.css                    # Creates styles.css.ssr\n" +
+            "spry-mkssr -o bundle.ssr app.js          # Creates bundle.ssr\n" +
+            "spry-mkssr --vala logo.png               # Creates LogoResource.vala\n" +
+            "spry-mkssr --vala --ns=MyApp styles.css  # Creates StylesResource.vala in MyApp namespace";
+        
+        var ssr_example = get_component_child<CodeBlockComponent>("ssr-example");
+        ssr_example.language = "Bash";
+        ssr_example.code = "# Generate an SSR file from a CSS file\n" +
+            "spry-mkssr styles.css\n" +
+            "# Output: styles.css.ssr\n\n" +
+            "# Generate with custom output name\n" +
+            "spry-mkssr -o resources/bundle.css.ssr styles.css\n" +
+            "# Output: resources/bundle.css.ssr\n\n" +
+            "# The resource name in URLs will be \"styles.css\"\n" +
+            "# URL: /_spry/res/styles.css";
+        
+        var vala_example = get_component_child<CodeBlockComponent>("vala-example");
+        vala_example.language = "Bash";
+        vala_example.code = "# Generate a Vala source file for embedding\n" +
+            "spry-mkssr --vala styles.css\n" +
+            "# Output: StylesResource.vala\n\n" +
+            "# The generated class name is derived from the filename\n" +
+            "# styles.css → StylesResource";
+        
+        var namespace_example = get_component_child<CodeBlockComponent>("namespace-example");
+        namespace_example.language = "Bash";
+        namespace_example.code = "# Generate with namespace\n" +
+            "spry-mkssr --vala --ns=MyApp.Static styles.css\n" +
+            "# Output: StylesResource.vala\n\n" +
+            "# Generated class:\n" +
+            "# namespace MyApp.Static {\n" +
+            "#     public class StylesResource : ConstantStaticResource {\n" +
+            "#         ...\n" +
+            "#     }\n" +
+            "# }";
+        
+        var class_structure = get_component_child<CodeBlockComponent>("class-structure");
+        class_structure.language = "Vala";
+        class_structure.code = "// Generated by spry-mkssr\n" +
+            "public class DocsResource : ConstantStaticResource {\n\n" +
+            "    // Resource metadata\n" +
+            "    public override string name { get { return \"docs.css\"; } }\n" +
+            "    public override string file_hash { get { return \"a1b2c3d4...\"; } }\n" +
+            "    public override string content_type { get { return \"text/css\"; } }\n\n" +
+            "    // Select best encoding based on client support\n" +
+            "    public override string get_best_encoding(Set<string> supported) {\n" +
+            "        if (supported.has(\"br\")) return \"br\";\n" +
+            "        if (supported.has(\"zstd\")) return \"zstd\";\n" +
+            "        if (supported.has(\"gzip\")) return \"gzip\";\n" +
+            "        return \"identity\";\n" +
+            "    }\n\n" +
+            "    // Return the data for a specific encoding\n" +
+            "    public override unowned uint8[] get_encoding(string encoding) {\n" +
+            "        switch (encoding) {\n" +
+            "            case \"br\": return BR_DATA;\n" +
+            "            case \"zstd\": return ZSTD_DATA;\n" +
+            "            case \"gzip\": return GZIP_DATA;\n" +
+            "            default: return IDENTITY_DATA;\n" +
+            "        }\n" +
+            "    }\n\n" +
+            "    // Embedded compressed data\n" +
+            "    private const uint8[] IDENTITY_DATA = { /* ... */ };\n" +
+            "    private const uint8[] GZIP_DATA = { /* ... */ };\n" +
+            "    private const uint8[] ZSTD_DATA = { /* ... */ };\n" +
+            "    private const uint8[] BR_DATA = { /* ... */ };\n" +
+            "}";
+        
+        var meson_example = get_component_child<CodeBlockComponent>("meson-example");
+        meson_example.language = "Meson";
+        meson_example.code = "# Generate a Vala resource file from CSS\n" +
+            "docs_css_resource = custom_target('docs-css-resource',\n" +
+            "    input: 'Static/docs.css',\n" +
+            "    output: 'DocsCssResource.vala',\n" +
+            "    command: [spry_mkssr, '--vala', '--ns=Demo.Static', \n" +
+            "              '-n', 'docs.css', '-c', 'text/css', \n" +
+            "              '-o', '@OUTPUT@', '@INPUT@']\n" +
+            ")\n\n" +
+            "# Then include in your executable sources:\n" +
+            "executable('my-app',\n" +
+            "    app_sources,\n" +
+            "    docs_css_resource,  # Add generated file\n" +
+            "    dependencies: [spry_dep]\n" +
+            ")";
+        
+        var meson_complete = get_component_child<CodeBlockComponent>("meson-complete");
+        meson_complete.language = "Meson";
+        meson_complete.code = "# Find the spry-mkssr tool\n" +
+            "spry_mkssr = find_program('spry-mkssr')\n\n" +
+            "# Generate multiple resources\n" +
+            "css_resource = custom_target('css-resource',\n" +
+            "    input: 'Static/styles.css',\n" +
+            "    output: 'StylesResource.vala',\n" +
+            "    command: [spry_mkssr, '--vala', '--ns=MyApp.Resources',\n" +
+            "              '-o', '@OUTPUT@', '@INPUT@']\n" +
+            ")\n\n" +
+            "js_resource = custom_target('js-resource',\n" +
+            "    input: 'Static/app.js',\n" +
+            "    output: 'AppResource.vala',\n" +
+            "    command: [spry_mkssr, '--vala', '--ns=MyApp.Resources',\n" +
+            "              '-c', 'application/javascript',\n" +
+            "              '-o', '@OUTPUT@', '@INPUT@']\n" +
+            ")\n\n" +
+            "# Build executable with embedded resources\n" +
+            "executable('my-app',\n" +
+            "    'Main.vala',\n" +
+            "    css_resource,\n" +
+            "    js_resource,\n" +
+            "    dependencies: [spry_dep]\n" +
+            ")";
+        
+        var compression_output = get_component_child<CodeBlockComponent>("compression-output");
+        compression_output.language = "Bash";
+        compression_output.code = "$ spry-mkssr --vala docs.css\n" +
+            "Compressing with gzip...\n" +
+            "  gzip: 16094 -> 3245 bytes (20.2%)\n" +
+            "Compressing with zstd...\n" +
+            "  zstd: 16094 -> 2987 bytes (18.6%)\n" +
+            "Compressing with brotli...\n" +
+            "  brotli: 16094 -> 2654 bytes (16.5%)\n" +
+            "Generated: DocsResource.vala";
+        
+        var content_type_example = get_component_child<CodeBlockComponent>("content-type-example");
+        content_type_example.language = "Bash";
+        content_type_example.code = "# Automatic detection (recommended)\n" +
+            "spry-mkssr styles.css        # → text/css\n" +
+            "spry-mkssr app.js            # → application/javascript\n" +
+            "spry-mkssr logo.png          # → image/png\n\n" +
+            "# Manual override\n" +
+            "spry-mkssr -c text/plain data.json   # Force text/plain\n" +
+            "spry-mkssr -c application/wasm module.wasm  # Custom MIME type";
+        
+        var naming_example = get_component_child<CodeBlockComponent>("naming-example");
+        naming_example.language = "Bash";
+        naming_example.code = "# Default naming\n" +
+            "spry-mkssr --vala htmx.min.js\n" +
+            "# Resource: \"htmx.min.js\", Class: HtmxMinResource\n\n" +
+            "# Custom resource name\n" +
+            "spry-mkssr --vala -n htmx.js htmx.min.js\n" +
+            "# Resource: \"htmx.js\", Class: HtmxResource\n" +
+            "# URL: /_spry/res/htmx.js\n\n" +
+            "# This is useful for:\n" +
+            "# - Hiding .min suffix from URLs\n" +
+            "# - Using versioned files with clean names\n" +
+            "# - Renaming resources for organization";
+    }
+}

+ 259 - 0
demo/Pages/StaticResourcesOverviewPage.vala

@@ -0,0 +1,259 @@
+using Spry;
+using Inversion;
+
+/**
+ * StaticResourcesOverviewPage - Introduction to Spry Static Resources
+ * 
+ * This page provides a comprehensive overview of static resource handling in Spry,
+ * including the different types of resources and how to use them.
+ */
+public class StaticResourcesOverviewPage : PageComponent {
+    
+    public const string ROUTE = "/static-resources/overview";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div sid="page" class="doc-content">
+            <h1>Static Resources Overview</h1>
+            
+            <section class="doc-section">
+                <h2>What are Static Resources?</h2>
+                <p>
+                    Spry provides a built-in system for serving static assets like CSS, JavaScript,
+                    images, and fonts. The static resource system handles content negotiation,
+                    compression, and caching automatically.
+                </p>
+                <p>
+                    Unlike traditional web frameworks that serve static files directly from disk,
+                    Spry's static resources are pre-compressed and can be embedded directly into
+                    your binary for single-binary deployments.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Types of Static Resources</h2>
+                <p>
+                    Spry provides three implementations of the 
+                    <code>StaticResource</code> interface:
+                </p>
+                
+                <div class="resource-types">
+                    <div class="resource-type-card">
+                        <h3>FileStaticResource</h3>
+                        <p>
+                            Reads pre-compiled <code>.ssr</code> (Spry Static Resource) files from
+                            disk at runtime. These files contain the resource data along with
+                            pre-compressed versions (gzip, zstd, brotli) and metadata.
+                        </p>
+                        <ul>
+                            <li>Loaded from <code>.ssr</code> files generated by spry-mkssr</li>
+                            <li>Contains all compression variants in a single file</li>
+                            <li>Includes SHA-512 hash for ETags</li>
+                        </ul>
+                    </div>
+                    
+                    <div class="resource-type-card">
+                        <h3>MemoryStaticResource</h3>
+                        <p>
+                            Holds static resource data entirely in memory. Useful for
+                            dynamically generated resources or when you want to load
+                            resources from a custom source.
+                        </p>
+                        <ul>
+                            <li>All data held in RAM</li>
+                            <li>Supports all compression formats</li>
+                            <li>Created programmatically at runtime</li>
+                        </ul>
+                    </div>
+                    
+                    <div class="resource-type-card">
+                        <h3>ConstantStaticResource</h3>
+                        <p>
+                            Embeds static resources directly into your binary at compile time.
+                            The resource data becomes part of your executable, enabling true
+                            single-binary deployments.
+                        </p>
+                        <ul>
+                            <li>Compiled into your binary</li>
+                            <li>Generated by spry-mkssr with <code>--vala</code> flag</li>
+                            <li>Zero runtime file I/O</li>
+                        </ul>
+                    </div>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The StaticResource Interface</h2>
+                <p>
+                    All static resources implement the <code>StaticResource</code> interface,
+                    which defines the contract for serving static content:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="interface-code"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>The StaticResourceProvider Endpoint</h2>
+                <p>
+                    Spry includes a built-in endpoint for serving static resources. The
+                    <code>StaticResourceProvider</code> handles all the complexity of content
+                    negotiation and caching.
+                </p>
+                
+                <h3>Route Pattern</h3>
+                <p>
+                    Static resources are served from the route:
+                </p>
+                <div class="code-inline">
+                    <code>/_spry/res/{resource}</code>
+                </div>
+                <p>
+                    Where <code>{resource}</code> is the name of the resource (e.g., 
+                    <code>docs.css</code>, <code>htmx.js</code>).
+                </p>
+                
+                <h3>Content Negotiation</h3>
+                <p>
+                    The provider automatically selects the best compression format based on
+                    the client's <code>Accept-Encoding</code> header. Supported encodings:
+                </p>
+                <ul>
+                    <li><strong>gzip</strong> - Universal browser support</li>
+                    <li><strong>zstd</strong> - Modern, high-performance compression</li>
+                    <li><strong>br</strong> (Brotli) - Best compression ratios for text</li>
+                    <li><strong>identity</strong> - No compression (always available)</li>
+                </ul>
+                
+                <h3>ETag-Based Caching</h3>
+                <p>
+                    Each resource generates an ETag based on its content hash and encoding.
+                    The provider handles <code>If-None-Match</code> requests, returning
+                    <code>304 Not Modified</code> when the client's cached version is current.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="etag-code"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Using Resources in Templates</h2>
+                <p>
+                    Spry provides the <code>spry-res</code> attribute for easily referencing
+                    static resources in your HTML templates. This attribute automatically
+                    generates the correct URL for your resource.
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="template-usage"/>
+                
+                <p>
+                    The <code>spry-res</code> attribute works with any element that has
+                    <code>href</code> or <code>src</code> attributes:
+                </p>
+                <ul>
+                    <li><code><link></code> for stylesheets</li>
+                    <li><code><script></code> for JavaScript</li>
+                    <li><code><img></code> for images</li>
+                    <li><code><source></code> for media elements</li>
+                </ul>
+            </section>
+            
+            <section class="doc-section">
+                <h2>How It Works</h2>
+                <div class="lifecycle-diagram">
+                    <div class="lifecycle-step">
+                        <div class="step-number">1</div>
+                        <div class="step-content">
+                            <h4>Build Time</h4>
+                            <p>Use spry-mkssr to generate .ssr files or Vala source files</p>
+                        </div>
+                    </div>
+                    <div class="lifecycle-arrow">↓</div>
+                    <div class="lifecycle-step">
+                        <div class="step-number">2</div>
+                        <div class="step-content">
+                            <h4>Registration</h4>
+                            <p>Register resources via dependency injection</p>
+                        </div>
+                    </div>
+                    <div class="lifecycle-arrow">↓</div>
+                    <div class="lifecycle-step">
+                        <div class="step-number">3</div>
+                        <div class="step-content">
+                            <h4>Request</h4>
+                            <p>Client requests /_spry/res/resource-name</p>
+                        </div>
+                    </div>
+                    <div class="lifecycle-arrow">↓</div>
+                    <div class="lifecycle-step">
+                        <div class="step-number">4</div>
+                        <div class="step-content">
+                            <h4>Negotiation</h4>
+                            <p>Server selects best encoding based on Accept-Encoding</p>
+                        </div>
+                    </div>
+                    <div class="lifecycle-arrow">↓</div>
+                    <div class="lifecycle-step">
+                        <div class="step-number">5</div>
+                        <div class="step-content">
+                            <h4>Response</h4>
+                            <p>Compressed content sent with ETag for caching</p>
+                        </div>
+                    </div>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/static-resources/spry-mkssr" class="nav-card">
+                        <h3>spry-mkssr Tool →</h3>
+                        <p>Learn how to generate static resources</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        var interface_code = get_component_child<CodeBlockComponent>("interface-code");
+        interface_code.language = "Vala";
+        interface_code.code = "public interface StaticResource : Object {\n" +
+            "    // The unique name of this resource (e.g., \"docs.css\")\n" +
+            "    public abstract string name { get; }\n\n" +
+            "    // Returns the best encoding supported by the client\n" +
+            "    public abstract string get_best_encoding(Set<string> supported);\n\n" +
+            "    // Returns the ETag for a specific encoding\n" +
+            "    public abstract string get_etag_for(string encoding);\n\n" +
+            "    // Converts the resource to an HTTP result\n" +
+            "    public abstract HttpResult to_result(string encoding, \n" +
+            "                                         StatusCode status = StatusCode.OK) \n" +
+            "        throws Error;\n" +
+            "}";
+        
+        var etag_code = get_component_child<CodeBlockComponent>("etag-code");
+        etag_code.language = "Vala";
+        etag_code.code = "// ETag format: \"{encoding}-{hash}\"\n" +
+            "// Example: \"br-a1b2c3d4e5f6...\"\n\n" +
+            "// The provider checks If-None-Match automatically:\n" +
+            "if (request.headers.get_or_empty(\"If-None-Match\").contains(etag)) {\n" +
+            "    return new HttpEmptyResult(StatusCode.NOT_MODIFIED);\n" +
+            "}\n\n" +
+            "// Fresh content is returned with the ETag header\n" +
+            "response.set_header(\"ETag\", etag);";
+        
+        var template_usage = get_component_child<CodeBlockComponent>("template-usage");
+        template_usage.language = "HTML";
+        template_usage.code = "<!-- Stylesheets -->\n" +
+            "<link rel=\"stylesheet\" spry-res=\"docs.css\">\n\n" +
+            "<!-- JavaScript -->\n" +
+            "<script spry-res=\"htmx.js\"></script>\n" +
+            "<script spry-res=\"htmx-sse.js\"></script>\n\n" +
+            "<!-- Images -->\n" +
+            "<img spry-res=\"logo.png\" alt=\"Logo\">\n\n" +
+            "<!-- Results in URLs like: -->\n" +
+            "<!-- /_spry/res/docs.css -->\n" +
+            "<!-- /_spry/res/htmx.js -->";
+    }
+}

+ 995 - 0
demo/Static/docs.css

@@ -0,0 +1,995 @@
+/* ==========================================================================
+   Documentation Site CSS Theme
+   A clean, minimal theme for documentation sites
+   ========================================================================== */
+
+/* ==========================================================================
+   CSS Custom Properties (Variables)
+   ========================================================================== */
+
+:root {
+  /* Colors - Light Mode */
+  --color-background: #ffffff;
+  --color-sidebar-bg: #f8f9fa;
+  --color-text: #24292f;
+  --color-text-muted: #57606a;
+  --color-primary: #0969da;
+  --color-primary-hover: #0550ae;
+  --color-border: #d0d7de;
+  --color-code-bg: #f6f8fa;
+  --color-code-border: #d0d7de;
+  --color-table-header-bg: #f6f8fa;
+  --color-blockquote-border: #d0d7de;
+  --color-blockquote-bg: #f6f8fa;
+  --color-hover-bg: #eaeef2;
+  --color-active-bg: #ddf4ff;
+  --color-active-border: #0969da;
+
+  /* Layout */
+  --sidebar-width: 280px;
+  --content-max-width: 800px;
+  --sidebar-padding: 1.5rem;
+  --content-padding: 2rem;
+
+  /* Typography */
+  --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
+  --font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+  --font-size-base: 16px;
+  --font-size-small: 0.875rem;
+  --font-size-smaller: 0.75rem;
+  --line-height-base: 1.65;
+  --line-height-heading: 1.25;
+
+  /* Spacing */
+  --spacing-xs: 0.25rem;
+  --spacing-sm: 0.5rem;
+  --spacing-md: 1rem;
+  --spacing-lg: 1.5rem;
+  --spacing-xl: 2rem;
+  --spacing-xxl: 3rem;
+
+  /* Transitions */
+  --transition-fast: 150ms ease;
+  --transition-normal: 250ms ease;
+}
+
+/* ==========================================================================
+   Base Reset & Typography
+   ========================================================================== */
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+}
+
+html {
+  font-size: var(--font-size-base);
+  scroll-behavior: smooth;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  font-family: var(--font-family);
+  font-size: 1rem;
+  line-height: var(--line-height-base);
+  color: var(--color-text);
+  background-color: var(--color-background);
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+/* Headings */
+h1, h2, h3, h4, h5, h6 {
+  margin: var(--spacing-xxl) 0 var(--spacing-md);
+  font-weight: 600;
+  line-height: var(--line-height-heading);
+  color: var(--color-text);
+}
+
+h1 {
+  font-size: 2rem;
+  padding-bottom: var(--spacing-md);
+  border-bottom: 1px solid var(--color-border);
+}
+
+h2 {
+  font-size: 1.5rem;
+  padding-bottom: var(--spacing-sm);
+  border-bottom: 1px solid var(--color-border);
+}
+
+h3 {
+  font-size: 1.25rem;
+}
+
+h4 {
+  font-size: 1.125rem;
+}
+
+h5 {
+  font-size: 1rem;
+}
+
+h6 {
+  font-size: var(--font-size-small);
+  color: var(--color-text-muted);
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+}
+
+/* First heading should not have top margin */
+h1:first-child,
+h2:first-child,
+h3:first-child {
+  margin-top: 0;
+}
+
+/* Paragraphs */
+p {
+  margin: var(--spacing-md) 0;
+}
+
+/* ==========================================================================
+   Layout
+   ========================================================================== */
+
+.docs-container {
+  display: flex;
+  min-height: 100vh;
+}
+
+/* Sidebar Navigation */
+.docs-sidebar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: var(--sidebar-width);
+  height: 100vh;
+  background-color: var(--color-sidebar-bg);
+  border-right: 1px solid var(--color-border);
+  overflow-y: auto;
+  padding: var(--sidebar-padding);
+  z-index: 100;
+}
+
+.docs-sidebar-header {
+  padding-bottom: var(--spacing-md);
+  margin-bottom: var(--spacing-md);
+  border-bottom: 1px solid var(--color-border);
+}
+
+.docs-sidebar-logo {
+  font-size: 1.25rem;
+  font-weight: 600;
+  color: var(--color-text);
+  text-decoration: none;
+}
+
+.docs-sidebar-logo:hover {
+  color: var(--color-primary);
+}
+
+/* Main Content Area */
+.docs-main {
+  margin-left: var(--sidebar-width);
+  flex: 1;
+  min-width: 0;
+}
+
+.docs-content {
+  max-width: var(--content-max-width);
+  margin: 0 auto;
+  padding: var(--content-padding);
+}
+
+/* ==========================================================================
+   Navigation Sidebar
+   ========================================================================== */
+
+/* NavSidebarComponent styles */
+.nav-sidebar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: var(--sidebar-width);
+  height: 100vh;
+  background-color: var(--color-sidebar-bg);
+  border-right: 1px solid var(--color-border);
+  overflow-y: auto;
+  padding: var(--sidebar-padding);
+  z-index: 100;
+}
+
+.nav-section {
+  margin-bottom: var(--spacing-md);
+}
+
+.nav-section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  padding: var(--spacing-sm) var(--spacing-md);
+  font-family: var(--font-family);
+  font-size: var(--font-size-small);
+  font-weight: 600;
+  color: var(--color-text);
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+  cursor: pointer;
+  background: none;
+  border: none;
+  border-radius: 4px;
+  transition: background-color var(--transition-fast);
+}
+
+.nav-section-header:hover {
+  background-color: var(--color-hover-bg);
+}
+
+.nav-section.expanded .nav-section-header {
+  background-color: var(--color-hover-bg);
+}
+
+.nav-items {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+.nav-item {
+  display: block;
+  padding: var(--spacing-sm) var(--spacing-md);
+  color: var(--color-text-muted);
+  text-decoration: none;
+  font-size: var(--font-size-small);
+  border-radius: 4px;
+  border-left: 3px solid transparent;
+  transition: all var(--transition-fast);
+}
+
+.nav-item:hover {
+  color: var(--color-text);
+  background-color: var(--color-hover-bg);
+  text-decoration: none;
+}
+
+.nav-item.active {
+  color: var(--color-primary);
+  background-color: var(--color-active-bg);
+  border-left-color: var(--color-active-border);
+  font-weight: 500;
+}
+
+.docs-nav {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+.docs-nav-section {
+  margin-bottom: var(--spacing-md);
+}
+
+.docs-nav-section-title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: var(--spacing-sm) var(--spacing-md);
+  font-size: var(--font-size-small);
+  font-weight: 600;
+  color: var(--color-text);
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+  cursor: pointer;
+  background: none;
+  border: none;
+  width: 100%;
+  text-align: left;
+  border-radius: 4px;
+  transition: background-color var(--transition-fast);
+}
+
+.docs-nav-section-title:hover {
+  background-color: var(--color-hover-bg);
+}
+
+.docs-nav-section-title::after {
+  content: "";
+  display: inline-block;
+  width: 6px;
+  height: 6px;
+  border-right: 2px solid currentColor;
+  border-bottom: 2px solid currentColor;
+  transform: rotate(-45deg);
+  transition: transform var(--transition-fast);
+}
+
+.docs-nav-section.collapsed .docs-nav-section-title::after {
+  transform: rotate(45deg);
+}
+
+.docs-nav-section.collapsed .docs-nav-items {
+  display: none;
+}
+
+.docs-nav-items {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+.docs-nav-item {
+  margin: 0;
+}
+
+.docs-nav-link {
+  display: block;
+  padding: var(--spacing-sm) var(--spacing-md);
+  color: var(--color-text-muted);
+  text-decoration: none;
+  font-size: var(--font-size-small);
+  border-radius: 4px;
+  border-left: 3px solid transparent;
+  transition: all var(--transition-fast);
+}
+
+.docs-nav-link:hover {
+  color: var(--color-text);
+  background-color: var(--color-hover-bg);
+}
+
+.docs-nav-link.active {
+  color: var(--color-primary);
+  background-color: var(--color-active-bg);
+  border-left-color: var(--color-active-border);
+  font-weight: 500;
+}
+
+/* Nested navigation items */
+.docs-nav-items .docs-nav-items {
+  margin-left: var(--spacing-md);
+}
+
+.docs-nav-items .docs-nav-items .docs-nav-link {
+  padding-left: var(--spacing-lg);
+  font-size: calc(var(--font-size-small) - 0.0625rem);
+}
+
+/* ==========================================================================
+   Links
+   ========================================================================== */
+
+a {
+  color: var(--color-primary);
+  text-decoration: none;
+  transition: color var(--transition-fast);
+}
+
+a:hover {
+  color: var(--color-primary-hover);
+  text-decoration: underline;
+}
+
+a:focus {
+  outline: 2px solid var(--color-primary);
+  outline-offset: 2px;
+}
+
+/* ==========================================================================
+   Code Blocks
+   ========================================================================== */
+
+code {
+  font-family: var(--font-family-mono);
+  font-size: 0.9em;
+  padding: var(--spacing-xs) var(--spacing-sm);
+  background-color: var(--color-code-bg);
+  border-radius: 4px;
+  border: 1px solid var(--color-code-border);
+}
+
+/* Inline code */
+p > code,
+li > code,
+td > code {
+  font-size: 0.85em;
+}
+
+/* Block code */
+pre {
+  margin: var(--spacing-lg) 0;
+  padding: var(--spacing-md);
+  background-color: var(--color-code-bg);
+  border: 1px solid var(--color-code-border);
+  border-radius: 6px;
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+pre code {
+  display: block;
+  padding: 0;
+  background: none;
+  border: none;
+  font-size: var(--font-size-small);
+  line-height: 1.5;
+  white-space: pre;
+}
+
+/* Code block with language indicator */
+pre[data-lang] {
+  position: relative;
+}
+
+pre[data-lang]::before {
+  content: attr(data-lang);
+  position: absolute;
+  top: var(--spacing-sm);
+  right: var(--spacing-sm);
+  font-size: var(--font-size-smaller);
+  color: var(--color-text-muted);
+  text-transform: uppercase;
+  font-family: var(--font-family);
+}
+
+/* Syntax highlighting base colors (for use with highlighters like Prism.js) */
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+  color: var(--color-text-muted);
+}
+
+.token.punctuation {
+  color: var(--color-text);
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol {
+  color: #0550ae;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin {
+  color: #0a3069;
+}
+
+.token.operator,
+.token.entity,
+.token.url {
+  color: #cf222e;
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+  color: #cf222e;
+}
+
+.token.function,
+.token.class-name {
+  color: #8250df;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+  color: #953800;
+}
+
+/* ==========================================================================
+   Tables
+   ========================================================================== */
+
+table {
+  width: 100%;
+  margin: var(--spacing-lg) 0;
+  border-collapse: collapse;
+  border-spacing: 0;
+  font-size: var(--font-size-small);
+}
+
+th,
+td {
+  padding: var(--spacing-sm) var(--spacing-md);
+  text-align: left;
+  border: 1px solid var(--color-border);
+}
+
+th {
+  background-color: var(--color-table-header-bg);
+  font-weight: 600;
+}
+
+tr:nth-child(even) {
+  background-color: var(--color-code-bg);
+}
+
+table code {
+  font-size: 0.9em;
+}
+
+/* ==========================================================================
+   Lists
+   ========================================================================== */
+
+ul,
+ol {
+  margin: var(--spacing-md) 0;
+  padding-left: var(--spacing-xl);
+}
+
+li {
+  margin: var(--spacing-xs) 0;
+}
+
+li > ul,
+li > ol {
+  margin: var(--spacing-xs) 0;
+}
+
+/* Definition lists */
+dl {
+  margin: var(--spacing-lg) 0;
+}
+
+dt {
+  font-weight: 600;
+  margin-top: var(--spacing-md);
+}
+
+dd {
+  margin: 0;
+  padding-left: var(--spacing-xl);
+  color: var(--color-text-muted);
+}
+
+/* ==========================================================================
+   Blockquotes
+   ========================================================================== */
+
+blockquote {
+  margin: var(--spacing-lg) 0;
+  padding: var(--spacing-md) var(--spacing-lg);
+  background-color: var(--color-blockquote-bg);
+  border-left: 4px solid var(--color-blockquote-border);
+  border-radius: 0 4px 4px 0;
+}
+
+blockquote p {
+  margin: 0;
+}
+
+blockquote p + p {
+  margin-top: var(--spacing-md);
+}
+
+blockquote code {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+/* ==========================================================================
+   Horizontal Rules
+   ========================================================================== */
+
+hr {
+  margin: var(--spacing-xxl) 0;
+  padding: 0;
+  border: none;
+  border-top: 1px solid var(--color-border);
+}
+
+/* ==========================================================================
+   Page Layout (for full-page components)
+   ========================================================================== */
+
+.page-container {
+  display: flex;
+  min-height: 100vh;
+}
+
+.main-content {
+  margin-left: var(--sidebar-width);
+  flex: 1;
+  min-width: 0;
+}
+
+.doc-content {
+  max-width: var(--content-max-width);
+  margin: 0 auto;
+  padding: var(--content-padding);
+}
+
+.doc-section {
+  margin-bottom: var(--spacing-xxl);
+}
+
+/* ==========================================================================
+   Home Page Styles
+   ========================================================================== */
+
+.home-hero {
+  text-align: center;
+  padding: var(--spacing-xxl) 0;
+  margin-bottom: var(--spacing-xl);
+  border-bottom: 1px solid var(--color-border);
+}
+
+.hero-title {
+  font-size: 3rem;
+  font-weight: 700;
+  margin: 0 0 var(--spacing-sm);
+  color: var(--color-text);
+  border-bottom: none;
+  padding-bottom: 0;
+}
+
+.hero-tagline {
+  font-size: 1.25rem;
+  color: var(--color-text-muted);
+  margin: 0;
+}
+
+.intro {
+  font-size: 1.125rem;
+  line-height: 1.7;
+  color: var(--color-text);
+}
+
+/* Quick Links Grid */
+.quick-links {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+  gap: var(--spacing-lg);
+  margin: var(--spacing-lg) 0;
+}
+
+.quick-link-card {
+  display: block;
+  padding: var(--spacing-lg);
+  background-color: var(--color-sidebar-bg);
+  border: 1px solid var(--color-border);
+  border-radius: 8px;
+  text-decoration: none;
+  transition: all var(--transition-fast);
+}
+
+.quick-link-card:hover {
+  background-color: var(--color-hover-bg);
+  border-color: var(--color-primary);
+  text-decoration: none;
+}
+
+.quick-link-card h3 {
+  margin: 0 0 var(--spacing-sm);
+  font-size: 1.125rem;
+  color: var(--color-primary);
+  border-bottom: none;
+  padding-bottom: 0;
+}
+
+.quick-link-card p {
+  margin: 0;
+  font-size: var(--font-size-small);
+  color: var(--color-text-muted);
+  line-height: 1.5;
+}
+
+/* Features List */
+.features-list {
+  list-style: none;
+  padding: 0;
+  margin: var(--spacing-lg) 0;
+}
+
+.features-list li {
+  padding: var(--spacing-sm) 0;
+  padding-left: var(--spacing-lg);
+  position: relative;
+}
+
+.features-list li::before {
+  content: "✓";
+  position: absolute;
+  left: 0;
+  color: var(--color-primary);
+  font-weight: 600;
+}
+
+.features-list li strong {
+  color: var(--color-text);
+}
+
+/* ==========================================================================
+   Demo Host Component
+   ========================================================================== */
+
+.demo-host {
+  margin: var(--spacing-xl) 0;
+}
+
+.demo-frame {
+  border: 1px solid var(--color-border);
+  border-radius: 6px;
+  overflow: hidden;
+  background-color: var(--color-background);
+}
+
+.demo-frame-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: var(--spacing-sm) var(--spacing-md);
+  background-color: var(--color-sidebar-bg);
+  border-bottom: 1px solid var(--color-border);
+}
+
+.demo-frame-title {
+  font-size: var(--font-size-small);
+  font-weight: 600;
+  color: var(--color-text);
+  margin: 0;
+}
+
+.demo-frame-content {
+  padding: var(--spacing-lg);
+}
+
+/* Demo toggle buttons */
+.demo-toggle-group {
+  display: flex;
+  gap: var(--spacing-sm);
+}
+
+.demo-toggle-btn {
+  padding: var(--spacing-xs) var(--spacing-sm);
+  font-family: var(--font-family);
+  font-size: var(--font-size-smaller);
+  font-weight: 500;
+  color: var(--color-text-muted);
+  background-color: transparent;
+  border: 1px solid var(--color-border);
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all var(--transition-fast);
+}
+
+.demo-toggle-btn:hover {
+  color: var(--color-text);
+  background-color: var(--color-hover-bg);
+}
+
+.demo-toggle-btn.active {
+  color: var(--color-primary);
+  background-color: var(--color-active-bg);
+  border-color: var(--color-primary);
+}
+
+.demo-toggle-btn:focus {
+  outline: 2px solid var(--color-primary);
+  outline-offset: 2px;
+}
+
+/* Source code view */
+.demo-source {
+  margin-top: 0;
+  border-top: 1px solid var(--color-border);
+  border-radius: 0;
+}
+
+.demo-source pre {
+  margin: 0;
+  border: none;
+  border-radius: 0;
+}
+
+/* ==========================================================================
+   Utility Classes
+   ========================================================================== */
+
+/* Text utilities */
+.text-muted {
+  color: var(--color-text-muted);
+}
+
+.text-small {
+  font-size: var(--font-size-small);
+}
+
+/* Spacing utilities */
+.mt-0 { margin-top: 0; }
+.mt-1 { margin-top: var(--spacing-sm); }
+.mt-2 { margin-top: var(--spacing-md); }
+.mt-3 { margin-top: var(--spacing-lg); }
+.mt-4 { margin-top: var(--spacing-xl); }
+
+.mb-0 { margin-bottom: 0; }
+.mb-1 { margin-bottom: var(--spacing-sm); }
+.mb-2 { margin-bottom: var(--spacing-md); }
+.mb-3 { margin-bottom: var(--spacing-lg); }
+.mb-4 { margin-bottom: var(--spacing-xl); }
+
+/* ==========================================================================
+   Responsive Design
+   ========================================================================== */
+
+/* Tablet and smaller */
+@media (max-width: 1024px) {
+  :root {
+    --sidebar-width: 240px;
+    --content-padding: 1.5rem;
+  }
+}
+
+/* Mobile */
+@media (max-width: 768px) {
+  :root {
+    --sidebar-width: 100%;
+    --content-padding: 1rem;
+  }
+
+  .docs-sidebar {
+    transform: translateX(-100%);
+    transition: transform var(--transition-normal);
+    width: 280px;
+    max-width: 85vw;
+  }
+
+  .docs-sidebar.open {
+    transform: translateX(0);
+  }
+
+  .nav-sidebar {
+    transform: translateX(-100%);
+    transition: transform var(--transition-normal);
+    width: 280px;
+    max-width: 85vw;
+  }
+
+  .nav-sidebar.open {
+    transform: translateX(0);
+  }
+
+  .docs-main {
+    margin-left: 0;
+  }
+
+  /* Mobile menu toggle */
+  .docs-menu-toggle {
+    position: fixed;
+    top: var(--spacing-md);
+    left: var(--spacing-md);
+    z-index: 101;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 40px;
+    height: 40px;
+    padding: 0;
+    background-color: var(--color-background);
+    border: 1px solid var(--color-border);
+    border-radius: 6px;
+    cursor: pointer;
+    transition: background-color var(--transition-fast);
+  }
+
+  .docs-menu-toggle:hover {
+    background-color: var(--color-hover-bg);
+  }
+
+  .docs-menu-toggle::before {
+    content: "";
+    display: block;
+    width: 18px;
+    height: 2px;
+    background-color: var(--color-text);
+    box-shadow: 0 -5px 0 var(--color-text), 0 5px 0 var(--color-text);
+  }
+
+  /* Overlay when sidebar is open */
+  .docs-overlay {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 99;
+    opacity: 0;
+    visibility: hidden;
+    transition: opacity var(--transition-normal), visibility var(--transition-normal);
+  }
+
+  .docs-overlay.visible {
+    opacity: 1;
+    visibility: visible;
+  }
+
+  /* Adjust typography for mobile */
+  h1 {
+    font-size: 1.75rem;
+  }
+
+  h2 {
+    font-size: 1.35rem;
+  }
+
+  h3 {
+    font-size: 1.15rem;
+  }
+
+  /* Scrollable tables on mobile */
+  table {
+    display: block;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+
+  /* Home page mobile adjustments */
+  .main-content {
+    margin-left: 0;
+  }
+
+  .hero-title {
+    font-size: 2rem;
+  }
+
+  .hero-tagline {
+    font-size: 1rem;
+  }
+
+  .quick-links {
+    grid-template-columns: 1fr;
+  }
+}
+
+/* Hide mobile menu toggle on desktop */
+@media (min-width: 769px) {
+  .docs-menu-toggle,
+  .docs-overlay {
+    display: none;
+  }
+}
+
+/* ==========================================================================
+   Print Styles
+   ========================================================================== */
+
+@media print {
+  .docs-sidebar,
+  .nav-sidebar,
+  .docs-menu-toggle,
+  .docs-overlay {
+    display: none !important;
+  }
+
+  .docs-main {
+    margin-left: 0 !important;
+  }
+
+  .docs-content {
+    max-width: none !important;
+    padding: 0 !important;
+  }
+
+  a {
+    color: var(--color-text) !important;
+    text-decoration: underline;
+  }
+
+  pre,
+  code {
+    border: none !important;
+    background-color: transparent !important;
+  }
+}

+ 22 - 3
demo/meson.build

@@ -5,10 +5,28 @@ website_sources = files(
     'Main.vala',
     'MainTemplate.vala',
     'Pages/HomePage.vala',
-    'Components/FeatureCardComponent.vala',
-    'Components/AuroraWaveComponent.vala',
+    'Pages/ComponentsOverviewPage.vala',
+    'Pages/ComponentsTemplateSyntaxPage.vala',
+    'Pages/ComponentsActionsPage.vala',
+    'Pages/ComponentsOutletsPage.vala',
+    'Pages/ComponentsContinuationsPage.vala',
+    'Pages/PageComponentsOverviewPage.vala',
+    'Pages/PageTemplatesPage.vala',
+    'Pages/StaticResourcesOverviewPage.vala',
+    'Pages/StaticResourcesMkssrPage.vala',
     'Components/CodeBlockComponent.vala',
-    'Components/StatCardComponent.vala',
+    'Components/DemoHostComponent.vala',
+    'Components/NavSidebarComponent.vala',
+    'DemoComponents/SimpleCounterDemo.vala',
+    'DemoComponents/TodoListDemo.vala',
+    'DemoComponents/ProgressDemo.vala',
+)
+
+# Generate Vala resource file from CSS
+docs_css_resource = custom_target('docs-css-resource',
+    input: 'Static/docs.css',
+    output: 'DocsCssResource.vala',
+    command: [spry_mkssr, '--vala', '--ns=Demo.Static', '-n', 'docs.css', '-c', 'text/css', '-o', '@OUTPUT@', '@INPUT@']
 )
 
 # Math library for particle physics (sin/cos in explode function)
@@ -16,6 +34,7 @@ m_dep = meson.get_compiler('c').find_library('m', required: false)
 
 executable('spry-demo',
     website_sources,
+    docs_css_resource,
     dependencies: [spry_dep, astralis_dep, invercargill_dep, inversion_dep, m_dep],
     install: true
 )

+ 399 - 0
important-notes-on-writing-documentation-pages.md

@@ -0,0 +1,399 @@
+# Important Notes on Writing Documentation Pages for Spry
+
+This document captures key learnings and patterns for creating documentation pages in the Spry demo site.
+
+## Page Structure
+
+### PageComponent Pattern
+
+Documentation pages extend `PageComponent` (not `Component`). PageComponent is special because it acts as both a Component AND an Endpoint, meaning it handles its own route.
+
+```vala
+public class Demo.Pages.ComponentsOverviewPage : PageComponent {
+    public override string markup { get {
+        return """...""";
+    }}
+    
+    public override async void prepare() throws Error {
+        // Setup code
+    }
+}
+```
+
+### Template Wrapping
+
+**IMPORTANT**: Page markup should NOT include the full HTML structure (`<!DOCTYPE html>`, `<html>`, `<head>`, `<body>`). The `MainTemplate` class automatically wraps page content with the proper HTML structure, including:
+
+- DOCTYPE and html tags
+- `<head>` with stylesheets and scripts
+- `<body>` with the page container
+
+Your page markup should start with the content container:
+
+```vala
+public override string markup { get {
+    return """
+    <div class="page-container">
+        <spry-component name="NavSidebarComponent" sid="nav"/>
+        <main class="main-content">
+            <!-- Your content here -->
+        </main>
+    </div>
+    """;
+}}
+```
+
+### Navigation Sidebar
+
+Every documentation page should include the NavSidebarComponent and set its `current_path` property in `prepare()`:
+
+```vala
+public override async void prepare() throws Error {
+    var nav = get_component_child<NavSidebarComponent>("nav");
+    nav.current_path = "/components/overview";
+}
+```
+
+## Required Imports
+
+```vala
+using Spry;
+using Astralis;      // Required for HttpContext
+using Inversion;     // Required for inject<T>()
+using Invercargill.DataStructures;  // Required for Series<T>
+```
+
+## String Handling and Code Examples
+
+### Triple-Quote String Escaping
+
+**CRITICAL**: Vala's triple-quoted strings (`"""`) CANNOT contain another triple-quoted string sequence. This means you cannot embed code examples that show triple-quoted strings directly.
+
+**Wrong** (will fail to compile):
+```vala
+// This breaks because """ appears inside """
+return """
+    <pre><code>
+public override string markup { get {
+    return """<div>content</div>""";  // SYNTAX ERROR!
+}}
+    </code></pre>
+""";
+```
+
+**Solution**: Use regular strings with `\n` for line breaks and string concatenation:
+
+```vala
+private const string COMPONENT_CODE = 
+    "public class MyComponent : Component {\n" +
+    "    public override string markup { get {\n" +
+    "        return \"\"\"<div>content</div>\"\"\";\n" +
+    "    }}\n" +
+    "}";
+```
+
+### Escaping Quotes in Code Examples
+
+When code contains double quotes, you need to escape them:
+- In triple-quoted strings: Use `\"` for literal quotes
+- In regular strings: Use `\\\"` (escaped backslash + escaped quote)
+
+```vala
+// Inside a triple-quoted string:
+"this[\"element\"].text_content = \"Hello\";"
+
+// Inside a regular string (more escaping needed):
+"this[\\\"element\\\"].text_content = \\\"Hello\\\";"
+```
+
+### JSON in Code Examples
+
+JSON strings in code examples need heavy escaping:
+
+```vala
+// Showing hx-vals in code:
+"this[\"button\"].set_attribute(\"hx-vals\", \"{\\\"id\\\": 123}\");"
+```
+
+## CodeBlockComponent Usage
+
+Use `CodeBlockComponent` for displaying code examples with syntax highlighting:
+
+```vala
+// In markup:
+"<spry-component name=\"CodeBlockComponent\" sid=\"example-code\"/>"
+
+// In prepare():
+var code_block = get_component_child<CodeBlockComponent>("example-code");
+code_block.language = "vala";
+code_block.code = YOUR_CODE_STRING;
+```
+
+Supported languages: `vala`, `xml`, `css`, `javascript`
+
+## DemoHostComponent Usage
+
+Use `DemoHostComponent` for interactive demos that show source code alongside a working demo:
+
+```vala
+// In markup:
+"<spry-component name=\"DemoHostComponent\" sid=\"demo\"/>"
+
+// In prepare():
+var demo = get_component_child<DemoHostComponent>("demo");
+demo.source_file = "DemoComponents/SimpleCounterDemo.vala";
+
+// Create and set the actual demo component
+var counter = factory.create<SimpleCounterDemo>();
+demo.set_outlet_child("demo-outlet", counter);
+```
+
+The DemoHostComponent provides:
+- Tabbed interface (Source / Demo)
+- Syntax-highlighted source code display
+- Live demo outlet
+
+## Component Lifecycle and State
+
+### New Instance Per Request
+
+**IMPORTANT**: Components are instantiated fresh on each request. You cannot store state directly in component instance fields between requests.
+
+**Wrong** (state won't persist):
+```vala
+public class MyComponent : Component {
+    private int counter = 0;  // Reset to 0 on every request!
+}
+```
+
+**Correct** (use a singleton store):
+```vala
+public class MyComponent : Component {
+    private MyStore store = inject<MyStore>();  // Singleton, persists
+}
+
+public class MyStore : Object {
+    public int counter { get; set; default = 0; }  // Persists between requests
+}
+```
+
+### prepare() Must Be Async
+
+The `prepare()` method must be marked `async`:
+
+```vala
+public override async void prepare() throws Error {
+    // ...
+}
+```
+
+## Dependency Injection
+
+Use the `inject<T>()` pattern for dependency injection:
+
+```vala
+private ComponentFactory factory = inject<ComponentFactory>();
+private HttpContext http_context = inject<HttpContext>();
+private MyStore store = inject<MyStore>();  // Must be registered in Main.vala
+```
+
+Register singleton stores in `demo/Main.vala`:
+
+```vala
+// In Main.vala configure() method:
+container.register<MyStore>(new MyStore());
+```
+
+## Series<T> Collection
+
+The `Series<T>` type from Invercargill is used for collections:
+
+```vala
+public Series<TodoItem> items { get; set; default = new Series<TodoItem>(); }
+
+// Add items:
+items.add(new TodoItem(1, "Task"));
+
+// Iterate:
+foreach (var item in items) {
+    // ...
+}
+
+// Get count:
+int count = items.length;  // NOT .size!
+
+// Create new series:
+var new_items = new Series<TodoItem>();
+```
+
+## Common Patterns
+
+### Element Selection and Modification
+
+```vala
+// Select by sid:
+this["my-element"].text_content = "Hello";
+this["my-element"].add_class("active");
+this["my-element"].set_attribute("data-id", "123");
+
+// Set inner HTML:
+this["container"].inner_html = "<span>Dynamic content</span>";
+```
+
+### Handling Actions
+
+```vala
+public async override void handle_action(string action) throws Error {
+    switch (action) {
+        case "Save":
+            // Get form data
+            var value = http_context.request.query_params.get_any_or_default("field_name");
+            // Process...
+            break;
+        case "Delete":
+            // ...
+            break;
+    }
+}
+```
+
+### Using Outlets for Child Components
+
+```vala
+// In markup:
+"<ul><spry-outlet sid=\"items-outlet\"/></ul>"
+
+// In prepare():
+var children = new Series<Renderable>();
+foreach (var item in store.items) {
+    var component = factory.create<ChildComponent>();
+    component.item_id = item.id;
+    children.add(component);
+}
+set_outlet_children("items-outlet", children);
+```
+
+## Routes and Registration
+
+### Route Convention
+
+Routes follow the pattern `/section/page-name`:
+- `/components/overview`
+- `/components/actions`
+- `/components/outlets`
+
+### Registration in Main.vala
+
+Pages are registered with the router:
+
+```vala
+// In Main.vala:
+router.add_route("/components/overview", typeof(ComponentsOverviewPage));
+```
+
+Components used in demos must be registered with the factory:
+
+```vala
+factory.register<SimpleCounterDemo>();
+factory.register<TodoListDemo>();
+```
+
+## File Organization
+
+```
+demo/
+├── Pages/                      # PageComponent classes
+│   ├── ComponentsOverviewPage.vala
+│   ├── ComponentsActionsPage.vala
+│   └── ...
+├── Components/                 # Shared UI components
+│   ├── NavSidebarComponent.vala
+│   ├── CodeBlockComponent.vala
+│   └── DemoHostComponent.vala
+├── DemoComponents/             # Demo-specific components
+│   ├── SimpleCounterDemo.vala
+│   ├── TodoListDemo.vala
+│   └── ProgressDemo.vala
+├── Static/
+│   └── docs.css               # Documentation styles
+├── Main.vala                   # App configuration and routing
+├── MainTemplate.vala           # HTML wrapper template
+└── meson.build                 # Build configuration
+```
+
+## Build Configuration
+
+Add new source files to `demo/meson.build`:
+
+```vala
+demo_sources = files(
+    'Main.vala',
+    'MainTemplate.vala',
+    'Pages/ComponentsOverviewPage.vala',
+    'Pages/ComponentsActionsPage.vala',
+    'DemoComponents/SimpleCounterDemo.vala',
+    # ... etc
+)
+```
+
+## Styling Conventions
+
+Documentation pages use CSS classes from `docs.css`:
+
+- `.doc-hero` - Hero section with title
+- `.doc-section` - Content sections
+- `.doc-card` - Card containers
+- `.doc-table` - Tables for reference
+- `.doc-example` - Code example containers
+- `.demo-container` - Interactive demo containers
+
+## Debugging Tips
+
+1. **Build errors with strings**: Check for unescaped quotes or triple-quote conflicts
+2. **inject<T>() not found**: Add `using Inversion;`
+3. **HttpContext not found**: Add `using Astralis;`
+4. **Property read-only**: Add `set;` to property definition
+5. **Method signature mismatch**: Ensure `prepare()` is `async`
+
+## Quick Reference: Page Template
+
+```vala
+using Spry;
+using Astralis;
+using Inversion;
+
+public class Demo.Pages.YourPage : PageComponent {
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div class="page-container">
+            <spry-component name="NavSidebarComponent" sid="nav"/>
+            <main class="main-content">
+                <div class="doc-hero">
+                    <h1>Your Page Title</h1>
+                    <p class="doc-subtitle">Brief description</p>
+                </div>
+                
+                <div class="doc-section">
+                    <h2>Section Title</h2>
+                    <p>Content...</p>
+                    
+                    <spry-component name="CodeBlockComponent" sid="example"/>
+                </div>
+            </main>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        var nav = get_component_child<NavSidebarComponent>("nav");
+        nav.current_path = "/your/path";
+        
+        var code = get_component_child<CodeBlockComponent>("example");
+        code.language = "vala";
+        code.code = "your code here";
+    }
+}
+```

+ 19 - 5
src/Component.vala

@@ -229,7 +229,7 @@ namespace Spry {
             transform_action_nodes(doc);
             transform_target_nodes(doc);
             transform_global_nodes(doc);
-            transform_script_nodes(doc);
+            transform_resource_nodes(doc);
             transform_dynamic_attributes(doc);
             transform_continuation_nodes(doc);
             remove_internal_sids(doc);
@@ -240,6 +240,9 @@ namespace Spry {
             var outlets = doc.select("//spry-outlet");
             foreach (var outlet in outlets) {
                 var nodes = new Series<MarkupNode>();
+                if(!outlet.has_attribute("sid")) {
+                    throw new ComponentError.INVALID_TEMPLATE("Tag spry-outlet must either define a content-expr or an sid");
+                }
                 foreach(var renderable in _children.get_or_empty(outlet.get_attribute("sid"))) {
                     var document = yield renderable.to_document();
                     nodes.add_all(document.body.children);
@@ -289,14 +292,25 @@ namespace Spry {
             }
         }
 
-        private void transform_script_nodes(MarkupDocument doc) {
-            var script_nodes = doc.select("//script[@spry-res]");
+        private void transform_resource_nodes(MarkupDocument doc) {
+            var script_nodes = doc.select("//*[@spry-res]");
             foreach(var node in script_nodes) {
                 var res = node.get_attribute("spry-res");
-                if(res != null) {
-                    node.set_attribute("src", "/_spry/res/" + res);
+                if(res == null) {
+                    throw new ComponentError.INVALID_TEMPLATE("Attribute spry-res must have a value");
                 }
+
                 node.remove_attribute("spry-res");
+                var path = "/_spry/res/" + res;
+                if(node.tag_name == "script" || node.tag_name == "img") {
+                    node.set_attribute("src", path);
+                    continue;
+                }
+
+                if(node.tag_name == "link" && node.get_attribute("rel") == "stylesheet") {
+                    node.set_attribute("href", path);
+                    continue;
+                }
             }
         }
 

+ 0 - 2
src/PageComponent.vala

@@ -13,9 +13,7 @@ namespace Spry {
             // Get templates as renderables in order of lowest to highest "rank"
             var renderables = scope.get_registrations(typeof(PageTemplate))
                 .select_many<Pair<Registration, TemplateRoutePrefix>>(r => r.get_metadata<TemplateRoutePrefix>().select_pairs<Registration, TemplateRoutePrefix>(p => r, p => p))
-                .debug_trace("Pre-filter")
                 .where(p => p.value2.matches_route(route_context))
-                .debug_trace("Post-filter")
                 .order_by<uint>(p => p.value2.rank)
                 .attempt_select<Object>(p => scope.resolve_registration (p.value1))
                 .cast<Renderable>()