Page Templates
Create reusable layouts that wrap your page components
What are Page Templates?
A PageTemplate is a special component that provides the outer HTML
structure for your pages. Templates define the ,
, , and common elements like
navigation headers and footers.
The key feature of a PageTemplate is the <spry-template-outlet/>
element, which marks where page content will be inserted. When a PageComponent
renders, it automatically gets nested inside matching templates.
The Template Outlet
The <spry-template-outlet/> element is the placeholder where
page content gets inserted. Every PageTemplate must include at least one outlet.
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Site</title>
<link rel="stylesheet" href="/styles/main.css">
</head>
<body>
<header>
<nav><!-- Site navigation --></nav>
</header>
<main>
<!-- Page content is inserted here -->
<spry-template-outlet />
</main>
<footer>
<p>© 2024 My Site</p>
</footer>
</body>
</html>
💡 How it works: 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.
Creating a MainTemplate
Here's a complete example of a site-wide template that provides the HTML document structure, head elements, navigation, and footer:
using Spry;
/**
* MainTemplate - Site-wide layout template
*
* Wraps all pages with common HTML structure,
* navigation, and footer.
*/
public class MainTemplate : PageTemplate {
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>My Spry Application</title>
<link rel="stylesheet" href="/styles/main.css">
<script spry-res="htmx.js"></script>
</head>
<body>
<header class="site-header">
<nav>
<a href="/" class="logo">My App</a>
<ul>
<li><a href="/docs">Docs</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<spry-template-outlet />
</main>
<footer class="site-footer">
<p>© 2024 My App. Built with Spry.</p>
</footer>
</body>
</html>
""";
}}
}
Template Registration
Templates are registered with a prefix that determines which routes they apply to. The prefix is passed via metadata during registration:
// In Main.vala:
var spry_cfg = application.configure_with<SpryConfigurator>();
// Register template with empty prefix (matches all routes)
spry_cfg.add_template<MainTemplate>("");
// The TemplateRoutePrefix is created internally from the string
// and used for route matching
In this example, the TemplateRoutePrefix("") with an empty string
matches all routes, making it the default site-wide template.
Route Prefix Matching
Templates can target specific sections of your site using route prefixes. The prefix determines which routes the template will wrap:
| Prefix | Matches Routes | Use Case |
|---|---|---|
"" (empty) |
All routes | Site-wide default template |
"/admin" |
/admin/*, /admin/settings, etc. | Admin section layout |
"/docs" |
/docs/*, /docs/getting-started, etc. | Documentation section |
"/api" |
/api/* routes | API documentation or explorer |
How Matching Works
The TemplateRoutePrefix.matches_route() 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.
// TemplateRoutePrefix matching logic (simplified)
public class TemplateRoutePrefix : Object {
public uint rank { get; private set; }
public string prefix { get; private set; }
public TemplateRoutePrefix(string prefix) {
this.prefix = prefix;
// Rank is the number of path segments
rank = prefix.split("/").length - 1;
}
public bool matches_route(RouteContext context) {
// Returns true if prefix segments match
// the beginning of the route segments
return context.matched_route.route_segments
.starts_with(this.prefix_segments);
}
}
// Example: prefix "/admin" matches:
// /admin ✓ (exact match)
// /admin/users ✓ (prefix match)
// /admin/settings ✓ (prefix match)
// /user/admin ✗ (not a prefix match)
Multiple Templates
You can have multiple templates for different sections of your site. Templates are sorted by rank (prefix depth) and applied from lowest to highest rank:
// In Main.vala:
var spry_cfg = application.configure_with<SpryConfigurator>();
// Site-wide template (rank 0, matches everything)
spry_cfg.add_template<MainTemplate>("");
// Admin section template (rank 1, matches /admin/*)
spry_cfg.add_template<AdminTemplate>("/admin");
// Documentation template (rank 1, matches /docs/*)
spry_cfg.add_template<DocsTemplate>("/docs");
// API docs template (rank 2, matches /docs/api/*)
spry_cfg.add_template<ApiDocsTemplate>("/docs/api");
Template Nesting Order
For a route like /admin/users, templates would be applied in order:
- MainTemplate (prefix: "") - rank 0
- AdminTemplate (prefix: "/admin") - rank 1
- PageComponent - The actual page content
Each template's outlet receives the content from the next item in the chain, creating nested layouts.
Section-Specific Template Example
Here's an example of a template specifically for the admin section that adds an admin sidebar:
using Spry;
/**
* AdminTemplate - Layout for admin section
*
* Adds an admin sidebar to all /admin/* pages.
*/
public class AdminTemplate : PageTemplate {
public override string markup { get {
return """
<div class="admin-layout">
<aside class="admin-sidebar">
<h3>Admin</h3>
<nav>
<a href="/admin">Dashboard</a>
<a href="/admin/users">Users</a>
<a href="/admin/settings">Settings</a>
</nav>
</aside>
<div class="admin-content">
<!-- Page content inserted here -->
<spry-template-outlet />
</div>
</div>
""";
}}
}
// Register with /admin prefix
// spry_cfg.add_template<AdminTemplate>("/admin");
Head Content Merging
When templates wrap pages, the elements are automatically
merged. If a PageComponent or nested template has content,
those elements are appended to the outer template's head.
This allows pages to add their own stylesheets, scripts, or meta tags while still benefiting from the template's common head elements.
⚠️ Note: Head merging only works when templates render
actual elements. Make sure your templates include
a proper HTML structure with head and body sections.
Template vs Component
| Feature | PageTemplate | Component |
|---|---|---|
| Base class | Component |
Component |
| Has markup | ✓ Yes | ✓ Yes |
| Contains outlet | ✓ Required | ✗ Optional |
| Route matching | By prefix | N/A |
| Wraps pages | ✓ Yes | ✗ No |
Best Practices
- Keep templates focused: Each template should handle one level of layout (site-wide, section-specific)
-
Use semantic HTML: Include proper
,,elements - Include common resources: Add shared stylesheets and scripts in your main template
- Plan your prefix hierarchy: Design your URL structure to work with template prefixes
- Don't duplicate content: Let templates handle repeated elements like navigation