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.

xml
<!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:

vala
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:

vala
// 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.

vala
// 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:

vala
// 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:

  1. MainTemplate (prefix: "") - rank 0
  2. AdminTemplate (prefix: "/admin") - rank 1
  3. 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:

vala
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

Next Steps