Composer & Assembler: Building HTML Like LEGO Bricks in True Pageless Architecture
Learn how to organize HTML templates and assemble complete pages using the Composer and Assembler pattern—turning static HTML files into dynamic, maintainable web pages without ASPX.
Introduction
In the [step-by-step demo], we built routes using inline HTML strings directly in Global.asax.cs. That approach works for understanding the core concept, but real websites need something more structured.
A production site might have dozens of pages sharing the same header, footer, and navigation. Articles need consistent layouts. Components like news cards and menu items repeat across multiple pages. Embedding all this HTML in C# strings becomes unmaintainable quickly.
The solution: move HTML into separate template files and assemble them programmatically.
This article introduces three engines that work together:
| Engine | Responsibility |
|---|---|
| TemplateEngine | Loads and caches HTML template files |
| Composer | Builds reusable HTML fragments from templates + data |
| Assembler | Orchestrates complete pages from multiple components |
Think of it like LEGO bricks. Each template is a brick. The Composer shapes individual bricks. The Assembler snaps them together into a complete structure.
Two Approaches to HTML Generation
Before diving in, it’s worth noting there are two common approaches to generating HTML dynamically:
| Approach | Technique | Characteristics |
|---|---|---|
| StringBuilder Append | Concatenate fragments directly | Speed, efficiency, minimal overhead |
| Placeholder Replacement | {{tokens}} + string.Replace | Maintainability, designer-friendly templates |
The first approach—used by high-traffic sites like GitHub—builds HTML by appending strings sequentially. It’s fast because there’s no search-and-replace overhead.
The second approach uses template files containing placeholder tokens like {{page_title}} or {{header}}. At runtime, the application loads the template and replaces each placeholder with actual content.
This article demonstrates the placeholder replacement approach. Templates remain as clean HTML files that designers can edit directly without touching C# code.
Visualizing the Assembly
Before writing any code, let’s understand what we’re building. We’ll look at:
- A complete HTML page (the end result)
- The same page divided into layout and components
- The layout with placeholders where components will be injected
The Complete HTML
Here’s a simple but complete webpage:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About Us - My Website</title>
<link rel="stylesheet" href="/css/global.css">
</head>
<body>
<!-- Header -->
<header class="site-header">
<div class="container">
<a href="/" class="logo">My Website</a>
<nav class="nav">
<a href="/" class="nav-link">Home</a>
<a href="/about" class="nav-link active">About</a>
<a href="/contact" class="nav-link">Contact</a>
</nav>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<div class="container">
<h1>About Us</h1>
<p>Welcome to our website. We've been serving customers since 2010.</p>
</div>
</main>
<!-- Footer -->
<footer class="site-footer">
<div class="container">
<p>© 2025 My Website. All rights reserved.</p>
</div>
</footer>
<script src="/js/main.js"></script>
</body>
</html>This is what the browser receives. Now let’s see how it breaks apart.
Divided into Layout and Components
Looking at the HTML above, we can identify distinct sections:
Layout (the page skeleton):
<!DOCTYPE html>
<html>
<head>...</head>
<body>
[HEADER GOES HERE]
[MAIN CONTENT GOES HERE]
[FOOTER GOES HERE]
[SCRIPTS GO HERE]
</body>
</html>Component: Header
<header class="site-header">
<div class="container">
<a href="/" class="logo">My Website</a>
<nav class="nav">
[MENU ITEMS GO HERE]
</nav>
</div>
</header>Component: Menu Item (repeats for each link)
<a href="/about" class="nav-link active">About</a>Component: Footer
<footer class="site-footer">
<div class="container">
<p>© 2025 My Website. All rights reserved.</p>
</div>
</footer>Component: Scripts
<script src="/js/main.js"></script>The main content varies per page, so it’s not a fixed component—it’s assembled differently for articles, galleries, contact forms, etc.
Layout with Placeholders
Now we replace those [SECTION GOES HERE] markers with actual placeholder tokens:
Layout Template: base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}}</title>
<link rel="stylesheet" href="/css/global.css">
{{extra_css}}
</head>
<body>
{{header}}
{{main_content}}
{{footer}}
{{scripts}}
</body>
</html>Component Template: header.html
<header class="site-header">
<div class="container">
<a href="/" class="logo">My Website</a>
<nav class="nav">
{{menu_items}}
</nav>
</div>
</header>Component Template: menu-item.html
<a href="{{item_url}}" class="nav-link{{active_class}}">{{item_label}}</a>Component Template: footer.html
<footer class="site-footer">
<div class="container">
<p>© {{current_year}} My Website. All rights reserved.</p>
</div>
</footer>Component Template: scripts.html
<script src="/js/main.js"></script>Notice the pattern:
{{page_title}}— single value replacement{{header}}— entire component injection{{menu_items}}— multiple items generated from data{{active_class}}— conditional value (empty or ” active”)
The templates are pure HTML. No C# syntax, no special tags. A designer can open these files and edit the markup directly.
Template Organization
With the concept clear, let’s organize the template files. Create this folder structure:
App_Data/
└── templates/
├── layouts/
│ ├── base.html
│ ├── article.html
│ └── gallery.html
├── components/
│ ├── header.html
│ ├── footer.html
│ ├── menu-item.html
│ ├── news-card.html
│ └── scripts.html
└── pages/
└── contact.htmlThree categories:
| Folder | Purpose | Example |
|---|---|---|
layouts/ | Page skeletons with major placeholders | base.html, article.html |
components/ | Reusable HTML fragments | header.html, footer.html, menu-item.html |
pages/ | Static page content (no database) | contact.html, about.html |
Why App_Data? In ASP.NET, this folder is protected—IIS won’t serve files from it directly. Templates are internal resources, not public URLs.
The TemplateEngine
The TemplateEngine is the foundation. It loads template files from disk and provides placeholder replacement methods.
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
namespace MyApp
{
public static class TemplateEngine
{
private static string _templatePath = null;
private static Dictionary<string, string> _cache = new Dictionary<string, string>();
private static object _cacheLock = new object();
// ══════════════════════════════════════════════
// Template Path
// ══════════════════════════════════════════════
private static string TemplatePath
{
get
{
if (_templatePath == null)
{
_templatePath = HttpContext.Current.Server.MapPath("~/App_Data/templates");
}
return _templatePath;
}
}
// ══════════════════════════════════════════════
// Template Loading Methods
// ══════════════════════════════════════════════
public static string GetLayout(string name)
{
return GetTemplate("layouts/" + name);
}
public static string GetComponent(string name)
{
return GetTemplate("components/" + name);
}
public static string GetPage(string name)
{
return GetTemplate("pages/" + name);
}
private static string GetTemplate(string relativePath)
{
string cacheKey = relativePath.ToLower();
// Check cache first
lock (_cacheLock)
{
if (_cache.ContainsKey(cacheKey))
{
return _cache[cacheKey];
}
}
// Load from file
string filePath = Path.Combine(TemplatePath, relativePath);
if (!filePath.EndsWith(".html"))
{
filePath += ".html";
}
if (!File.Exists(filePath))
{
return $"<!-- Template not found: {relativePath} -->";
}
string content = File.ReadAllText(filePath);
// Store in cache
lock (_cacheLock)
{
_cache[cacheKey] = content;
}
return content;
}
// ══════════════════════════════════════════════
// Placeholder Replacement Methods
// ══════════════════════════════════════════════
public static string Replace(string template, string placeholder, string value)
{
return template.Replace("{{" + placeholder + "}}", value ?? "");
}
public static string ReplaceAll(string template, Dictionary<string, string> replacements)
{
foreach (var kvp in replacements)
{
template = template.Replace("{{" + kvp.Key + "}}", kvp.Value ?? "");
}
return template;
}
// ══════════════════════════════════════════════
// Encoding Helpers
// ══════════════════════════════════════════════
public static string Encode(string value)
{
return HttpUtility.HtmlEncode(value ?? "");
}
public static string AttrEncode(string value)
{
return HttpUtility.HtmlAttributeEncode(value ?? "");
}
// ══════════════════════════════════════════════
// Cache Management
// ══════════════════════════════════════════════
public static void ClearCache()
{
lock (_cacheLock)
{
_cache.Clear();
}
}
}
}Key points:
- Three loading methods —
GetLayout(),GetComponent(),GetPage()map to folder structure - In-memory caching — Templates load once, then serve from memory
- Simple replacement —
Replace()for single values,ReplaceAll()for multiple - Security encoding —
Encode()for content,AttrEncode()for HTML attributes
Usage example:
// Load a layout
string layout = TemplateEngine.GetLayout("base");
// Replace a single placeholder
layout = TemplateEngine.Replace(layout, "page_title", "About Us - My Website");
// Replace multiple placeholders
var replacements = new Dictionary<string, string>
{
{ "header", headerHtml },
{ "main_content", contentHtml },
{ "footer", footerHtml },
{ "scripts", scriptsHtml }
};
string finalHtml = TemplateEngine.ReplaceAll(layout, replacements);The Composer
The Composer builds HTML fragments. Each component has a dedicated method that loads a template, fetches any required data, and returns composed HTML.
using System;
using System.Collections.Generic;
using System.Text;
namespace MyApp
{
public static class Composer
{
// ══════════════════════════════════════════════
// Header Component
// ══════════════════════════════════════════════
public static string ComposeHeader(string activeSlug = "")
{
string template = TemplateEngine.GetComponent("header");
string menuItems = ComposeMainMenu(activeSlug);
return TemplateEngine.Replace(template, "menu_items", menuItems);
}
public static string ComposeMainMenu(string activeSlug = "")
{
var sb = new StringBuilder();
string itemTemplate = TemplateEngine.GetComponent("menu-item");
// Menu data - in production, this comes from database
var menuItems = new List<MenuItem>
{
new MenuItem { Label = "Home", Url = "/" },
new MenuItem { Label = "About", Url = "/about" },
new MenuItem { Label = "News", Url = "/news" },
new MenuItem { Label = "Contact", Url = "/contact" }
};
foreach (var item in menuItems)
{
// Determine if this item is active
string activeClass = "";
if (item.Url == "/" + activeSlug || (item.Url == "/" && string.IsNullOrEmpty(activeSlug)))
{
activeClass = " active";
}
var replacements = new Dictionary<string, string>
{
{ "item_url", TemplateEngine.AttrEncode(item.Url) },
{ "item_label", TemplateEngine.Encode(item.Label) },
{ "active_class", activeClass }
};
sb.Append(TemplateEngine.ReplaceAll(itemTemplate, replacements));
}
return sb.ToString();
}
// ══════════════════════════════════════════════
// Footer Component
// ══════════════════════════════════════════════
public static string ComposeFooter()
{
string template = TemplateEngine.GetComponent("footer");
return TemplateEngine.Replace(template, "current_year", DateTime.Now.Year.ToString());
}
// ══════════════════════════════════════════════
// Scripts Component
// ══════════════════════════════════════════════
public static string ComposeScripts()
{
return TemplateEngine.GetComponent("scripts");
}
// ══════════════════════════════════════════════
// News Cards Component
// ══════════════════════════════════════════════
public static string ComposeNewsCards(List<Article> articles)
{
if (articles == null || articles.Count == 0) return "";
var sb = new StringBuilder();
string cardTemplate = TemplateEngine.GetComponent("news-card");
int index = 1;
foreach (var article in articles)
{
var replacements = new Dictionary<string, string>
{
{ "item_url", "/" + TemplateEngine.AttrEncode(article.Slug) },
{ "item_index", index.ToString() },
{ "item_title", TemplateEngine.Encode(article.Title) },
{ "item_date", article.PublishDate.ToString("MMMM d, yyyy") },
{ "image_url", TemplateEngine.AttrEncode(article.ImageUrl) }
};
sb.Append(TemplateEngine.ReplaceAll(cardTemplate, replacements));
index++;
}
return sb.ToString();
}
}
// ══════════════════════════════════════════════
// Supporting Models
// ══════════════════════════════════════════════
public class MenuItem
{
public string Label { get; set; }
public string Url { get; set; }
}
public class Article
{
public string Title { get; set; }
public string Slug { get; set; }
public string ImageUrl { get; set; }
public DateTime PublishDate { get; set; }
}
}Key points:
- One method per component —
ComposeHeader(),ComposeFooter(),ComposeNewsCards() - StringBuilder for loops — When building multiple items, append to StringBuilder
- Data flows in — Methods receive data as parameters or fetch from database
- HTML flows out — Each method returns a composed HTML string
- Always encode — User data goes through
Encode()orAttrEncode()
The pattern for list components:
public static string ComposeItemList(List<Item> items)
{
var sb = new StringBuilder();
string template = TemplateEngine.GetComponent("item-template");
foreach (var item in items)
{
var replacements = new Dictionary<string, string>
{
{ "placeholder1", value1 },
{ "placeholder2", value2 }
};
sb.Append(TemplateEngine.ReplaceAll(template, replacements));
}
return sb.ToString();
}Load template once, loop through data, append each rendered item.
The Assembler
The Assembler is the orchestrator. It determines what type of page to build, calls the appropriate Composer methods, and assembles everything into final HTML.
using System;
using System.Collections.Generic;
namespace MyApp
{
public static class Assembler
{
// ══════════════════════════════════════════════
// Main Entry Point
// ══════════════════════════════════════════════
public static AssembleResult AssemblePage(string slug)
{
slug = (slug ?? "").Trim().TrimStart('/');
// Route to appropriate assembly method
switch (slug)
{
case "":
return AssembleHomepage();
case "about":
return AssembleAboutPage();
case "contact":
return AssembleContactPage();
default:
// Check if it's an article
var article = GetArticleBySlug(slug);
if (article != null)
{
return AssembleArticlePage(article);
}
// Not found
return Assemble404Page();
}
}
// ══════════════════════════════════════════════
// Homepage Assembly
// ══════════════════════════════════════════════
public static AssembleResult AssembleHomepage()
{
string layout = TemplateEngine.GetLayout("base");
// Get recent articles for news section
var recentArticles = GetRecentArticles(6);
var replacements = new Dictionary<string, string>
{
{ "page_title", "Welcome - My Website" },
{ "extra_css", "" },
{ "header", Composer.ComposeHeader("") },
{ "main_content", ComposeHomepageContent(recentArticles) },
{ "footer", Composer.ComposeFooter() },
{ "scripts", Composer.ComposeScripts() }
};
string html = TemplateEngine.ReplaceAll(layout, replacements);
return new AssembleResult
{
Success = true,
Html = html
};
}
private static string ComposeHomepageContent(List<Article> articles)
{
// For homepage, we might compose multiple sections
var sb = new StringBuilder();
sb.Append("<main class=\"main-content\">");
sb.Append("<div class=\"container\">");
sb.Append("<h1>Latest News</h1>");
sb.Append("<div class=\"news-grid\">");
sb.Append(Composer.ComposeNewsCards(articles));
sb.Append("</div>");
sb.Append("</div>");
sb.Append("</main>");
return sb.ToString();
}
// ══════════════════════════════════════════════
// Article Page Assembly
// ══════════════════════════════════════════════
public static AssembleResult AssembleArticlePage(Article article)
{
string layout = TemplateEngine.GetLayout("base");
string mainContent = $@"
<main class=""main-content"">
<div class=""container"">
<article class=""article"">
<h1>{TemplateEngine.Encode(article.Title)}</h1>
<time class=""article__date"">{article.PublishDate:MMMM d, yyyy}</time>
<div class=""article__content"">
{article.Content}
</div>
</article>
</div>
</main>";
var replacements = new Dictionary<string, string>
{
{ "page_title", article.Title + " - My Website" },
{ "extra_css", "" },
{ "header", Composer.ComposeHeader(article.Slug) },
{ "main_content", mainContent },
{ "footer", Composer.ComposeFooter() },
{ "scripts", Composer.ComposeScripts() }
};
string html = TemplateEngine.ReplaceAll(layout, replacements);
return new AssembleResult
{
Success = true,
Html = html
};
}
// ══════════════════════════════════════════════
// Static Pages Assembly
// ══════════════════════════════════════════════
public static AssembleResult AssembleAboutPage()
{
string layout = TemplateEngine.GetLayout("base");
string content = TemplateEngine.GetPage("about");
var replacements = new Dictionary<string, string>
{
{ "page_title", "About Us - My Website" },
{ "extra_css", "" },
{ "header", Composer.ComposeHeader("about") },
{ "main_content", content },
{ "footer", Composer.ComposeFooter() },
{ "scripts", Composer.ComposeScripts() }
};
string html = TemplateEngine.ReplaceAll(layout, replacements);
return new AssembleResult
{
Success = true,
Html = html
};
}
public static AssembleResult AssembleContactPage()
{
string layout = TemplateEngine.GetLayout("base");
string content = TemplateEngine.GetPage("contact");
var replacements = new Dictionary<string, string>
{
{ "page_title", "Contact Us - My Website" },
{ "extra_css", "" },
{ "header", Composer.ComposeHeader("contact") },
{ "main_content", content },
{ "footer", Composer.ComposeFooter() },
{ "scripts", Composer.ComposeScripts() }
};
string html = TemplateEngine.ReplaceAll(layout, replacements);
return new AssembleResult
{
Success = true,
Html = html
};
}
// ══════════════════════════════════════════════
// 404 Page Assembly
// ══════════════════════════════════════════════
public static AssembleResult Assemble404Page()
{
string layout = TemplateEngine.GetLayout("base");
string mainContent = @"
<main class=""main-content"">
<div class=""container"">
<h1>Page Not Found</h1>
<p>Sorry, the page you're looking for doesn't exist.</p>
<p><a href=""/"">Return to homepage</a></p>
</div>
</main>";
var replacements = new Dictionary<string, string>
{
{ "page_title", "Page Not Found - My Website" },
{ "extra_css", "" },
{ "header", Composer.ComposeHeader("") },
{ "main_content", mainContent },
{ "footer", Composer.ComposeFooter() },
{ "scripts", Composer.ComposeScripts() }
};
string html = TemplateEngine.ReplaceAll(layout, replacements);
return new AssembleResult
{
Success = false,
Html = html,
StatusCode = 404
};
}
// ══════════════════════════════════════════════
// Data Access (Mock - replace with database)
// ══════════════════════════════════════════════
private static Article GetArticleBySlug(string slug)
{
// In production: query database
// Demo: return mock data
if (slug == "welcome-to-our-site")
{
return new Article
{
Title = "Welcome to Our Site",
Slug = "welcome-to-our-site",
Content = "<p>This is our first article.</p>",
ImageUrl = "/images/welcome.jpg",
PublishDate = new DateTime(2025, 1, 1)
};
}
return null;
}
private static List<Article> GetRecentArticles(int count)
{
// In production: query database
// Demo: return mock data
return new List<Article>
{
new Article
{
Title = "Welcome to Our Site",
Slug = "welcome-to-our-site",
ImageUrl = "/images/welcome.jpg",
PublishDate = new DateTime(2025, 1, 1)
},
new Article
{
Title = "Our Second Post",
Slug = "our-second-post",
ImageUrl = "/images/second.jpg",
PublishDate = new DateTime(2025, 1, 5)
}
};
}
}
// ══════════════════════════════════════════════
// Assembly Result
// ══════════════════════════════════════════════
public class AssembleResult
{
public bool Success { get; set; }
public string Html { get; set; }
public int StatusCode { get; set; } = 200;
public string RedirectUrl { get; set; }
public bool Is301Redirect { get; set; }
}
}Key points:
- Single entry point —
AssemblePage(slug)handles all requests - Route by slug — Switch statement or pattern matching determines page type
- One method per page type —
AssembleHomepage(),AssembleArticlePage(),Assemble404Page() - Calls Composer — Header, footer, and other components come from Composer
- Returns result object — HTML plus metadata (status code, redirects)
Request-to-Response Flow
Let’s trace a request from URL to final HTML.
Request: https://mywebsite.com/about
┌─────────────────────────────────────────────────────────────────┐
│ 1. Browser requests /about │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. Application_BeginRequest intercepts │
│ → Extracts slug: "about" │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Assembler.AssemblePage("about") │
│ → Matches "about" case │
│ → Calls AssembleAboutPage() │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. AssembleAboutPage() │
│ → TemplateEngine.GetLayout("base") │
│ → TemplateEngine.GetPage("about") │
│ → Composer.ComposeHeader("about") │
│ → TemplateEngine.GetComponent("header") │
│ → Composer.ComposeMainMenu("about") │
│ → TemplateEngine.GetComponent("menu-item") × N │
│ → Composer.ComposeFooter() │
│ → TemplateEngine.GetComponent("footer") │
│ → Composer.ComposeScripts() │
│ → TemplateEngine.GetComponent("scripts") │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. TemplateEngine.ReplaceAll() │
│ → Replaces {{page_title}} with "About Us - My Website" │
│ → Replaces {{header}} with composed header HTML │
│ → Replaces {{main_content}} with about page content │
│ → Replaces {{footer}} with composed footer HTML │
│ → Replaces {{scripts}} with scripts HTML │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. Response.Write(html) │
│ → Complete HTML sent to browser │
└─────────────────────────────────────────────────────────────────┘Each box represents a distinct responsibility:
- Request handling — Extract what’s being asked for
- Assembler — Decide what to build, orchestrate the process
- Composer — Build individual components
- TemplateEngine — Load templates, perform replacements
- Response — Send the result
Connecting to Application_BeginRequest
In the demo article, we handled routes directly in Application_BeginRequest. Now we delegate to the Assembler:
using System;
using System.Web;
namespace MyApp
{
public class Global : HttpApplication
{
protected void Application_BeginRequest(object sender, EventArgs e)
{
string path = Request.Path.ToLower();
// Skip static files
if (IsStaticFile(path))
{
return; // Let IIS handle it
}
// Extract slug from path
string slug = path.TrimStart('/');
// Assemble the page
var result = Assembler.AssemblePage(slug);
// Handle redirect
if (result.Is301Redirect && !string.IsNullOrEmpty(result.RedirectUrl))
{
Response.RedirectPermanent(result.RedirectUrl);
return;
}
// Set status code
Response.StatusCode = result.StatusCode;
// Write HTML
Response.ContentType = "text/html";
Response.Write(result.Html);
EndResponse();
}
private bool IsStaticFile(string path)
{
return path.EndsWith(".css") || path.EndsWith(".js") ||
path.EndsWith(".png") || path.EndsWith(".jpg") ||
path.EndsWith(".jpeg") || path.EndsWith(".gif") ||
path.EndsWith(".ico") || path.EndsWith(".svg") ||
path.EndsWith(".woff") || path.EndsWith(".woff2");
}
private void EndResponse()
{
Response.Flush();
Response.SuppressContent = true;
HttpContext.Current.ApplicationInstance.CompleteRequest();
}
}
}The Application_BeginRequest is now minimal—it extracts the slug, calls the Assembler, and writes the response. All the complexity lives in well-organized classes.
Complete Working Example
Here’s everything together in a minimal, runnable form.
Project Structure
MyApp/
├── App_Data/
│ └── templates/
│ ├── layouts/
│ │ └── base.html
│ ├── components/
│ │ ├── header.html
│ │ ├── menu-item.html
│ │ ├── footer.html
│ │ └── scripts.html
│ └── pages/
│ ├── about.html
│ └── contact.html
├── Global.asax
├── Global.asax.cs
├── Engines/
│ ├── TemplateEngine.cs
│ ├── Composer.cs
│ └── Assembler.cs
└── Web.configTemplate Files
layouts/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; line-height: 1.6; }
.container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
.site-header { background: #333; color: white; padding: 1rem 0; }
.site-header .container { display: flex; justify-content: space-between; align-items: center; }
.logo { color: white; text-decoration: none; font-size: 1.5rem; font-weight: bold; }
.nav { display: flex; gap: 1rem; }
.nav-link { color: white; text-decoration: none; padding: 0.5rem 1rem; }
.nav-link:hover, .nav-link.active { background: rgba(255,255,255,0.1); }
.main-content { padding: 2rem 0; min-height: 60vh; }
.site-footer { background: #222; color: #999; padding: 2rem 0; text-align: center; }
</style>
{{extra_css}}
</head>
<body>
{{header}}
{{main_content}}
{{footer}}
{{scripts}}
</body>
</html>components/header.html
<header class="site-header">
<div class="container">
<a href="/" class="logo">My Website</a>
<nav class="nav">
{{menu_items}}
</nav>
</div>
</header>components/menu-item.html
<a href="{{item_url}}" class="nav-link{{active_class}}">{{item_label}}</a>components/footer.html
<footer class="site-footer">
<div class="container">
<p>© {{current_year}} My Website. All rights reserved.</p>
</div>
</footer>components/scripts.html
<script>
console.log('Page loaded');
</script>pages/about.html
<main class="main-content">
<div class="container">
<h1>About Us</h1>
<p>Welcome to our website. We've been serving customers since 2010.</p>
<p>Our mission is to provide quality content and excellent service.</p>
</div>
</main>pages/contact.html
<main class="main-content">
<div class="container">
<h1>Contact Us</h1>
<p>Email: hello@mywebsite.com</p>
<p>Phone: (555) 123-4567</p>
</div>
</main>Global.asax
<%@ Application Language="C#" CodeBehind="Global.asax.cs" Inherits="MyApp.Global" %>Test URLs
Once running, visit:
http://localhost/— Homepagehttp://localhost/about— About pagehttp://localhost/contact— Contact pagehttp://localhost/anything-else— 404 page
Each page shares the same header, footer, and navigation. Change header.html once, and every page updates.
Conclusion
The Composer and Assembler pattern transforms how we build pages:
- Templates are LEGO bricks — Small, reusable, composable
- Separation of concerns — Designers edit HTML, developers write C#
- Single source of truth — Change a component once, it updates everywhere
- Testable — Each piece can be tested independently
The three engines work in harmony:
| Engine | Does |
|---|---|
| TemplateEngine | Loads templates, replaces placeholders |
| Composer | Builds components from templates + data |
| Assembler | Orchestrates complete pages |
This is production-ready architecture. Add database queries to Composer methods, create more templates, and scale to hundreds of pages—all while keeping the code organized and maintainable.
For caching strategies to optimize performance in production, see [Part 1: Lightning Fast Page Caching Strategy for High Traffic Performance Vanilla ASP.NET Web Forms].
