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(); public override string markup { get { return """

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.

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

Template Registration

Templates are registered with a prefix that determines which routes they apply to. The prefix is passed via metadata during registration:

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.

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:

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:

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

"""; }} public override async void prepare() throws Error { // Outlet example var outlet_example = get_component_child("outlet-example"); outlet_example.language = "xml"; outlet_example.code = "\n" + "\n" + "\n" + " My Site\n" + " \n" + "\n" + "\n" + "
\n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + "

© 2024 My Site

\n" + "
\n" + "\n" + ""; // Main template example var main_template = get_component_child("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" + " \n" + " \n" + " \n" + " \n" + " \n" + " My Spry Application\n" + " \n" + " \n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "
\n" + " \n" + "
\n" + "

© 2024 My App. Built with Spry.

\n" + "
\n" + " \n" + " \n" + " \"\"\";\n" + " }}\n" + "}"; // Template registration var template_reg = get_component_child("template-registration"); template_reg.language = "vala"; template_reg.code = "// In Main.vala:\n\n" + "var spry_cfg = application.configure_with();\n\n" + "// Register template with empty prefix (matches all routes)\n" + "spry_cfg.add_template(\"\");\n\n" + "// The TemplateRoutePrefix is created internally from the string\n" + "// and used for route matching"; // Matching logic var matching_logic = get_component_child("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("multiple-templates"); multiple_templates.language = "vala"; multiple_templates.code = "// In Main.vala:\n\n" + "var spry_cfg = application.configure_with();\n\n" + "// Site-wide template (rank 0, matches everything)\n" + "spry_cfg.add_template(\"\");\n\n" + "// Admin section template (rank 1, matches /admin/*)\n" + "spry_cfg.add_template(\"/admin\");\n\n" + "// Documentation template (rank 1, matches /docs/*)\n" + "spry_cfg.add_template(\"/docs\");\n\n" + "// API docs template (rank 2, matches /docs/api/*)\n" + "spry_cfg.add_template(\"/docs/api\");"; // Admin template example var admin_template = get_component_child("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" + "
\n" + " \n" + " \n" + "
\n" + " \n" + " \n" + "
\n" + "
\n" + " \"\"\";\n" + " }}\n" + "}\n\n" + "// Register with /admin prefix\n" + "// spry_cfg.add_template(\"/admin\");"; } }