Templating
Chloroplast templates are built using ASP.NET's Razor templates.
Configuration
The SiteConfig.yml file lets you configure the location of templates and the folders to find content files to process.
# razor templates
templates_folder: templates
areas:
- source_folder: /source
output_folder: /
Markdown front matter
The template will be defined by the content's front matter.
---
template: TemplateName
title: Document title
---
If this value is either incorrect, or omitted, it will default to look for a template named Default.cshtml. These templates are assumed to be just for the content area itself.
SiteFrame.cshtml
For the main site's chrome, Chloroplast will look for a template named SiteFrame.cshtml by default. This is the outer "frame" or "chrome" that wraps your content.
You can also specify a different frame template for individual content files by adding a frame: property to the frontmatter:
---
template: TemplateName
frame: CustomFrame
title: Document title
---
If no frame: is specified, Chloroplast defaults to SiteFrame.cshtml.
Frame Template Example
The SiteFrame template renders the full page structure:
<html>
<head>
<title>@Model.GetMeta("Title")</title>
</head>
<body>
<div class="maincontent">
@Raw(Model.Body)
</div>
</body>
</html>
Error Handling
If a specified frame template is not found:
- An error message is logged to the console
- The affected content file is skipped (no incomplete page is rendered)
- The build continues processing other files
- The error is included in the build error summary
This ensures the build process doesn't stop completely due to a missing frame, but you'll be notified of the issue.
Customizing Templates
When designing custom templates for Chloroplast, focus on leveraging the available template methods and ensuring your templates handle the full range of Chloroplast features effectively.
Template Structure Reference
For recommended template structure and patterns, refer to the included templates:
- SiteFrame.cshtml - The main site chrome template that provides the overall page structure, navigation, and asset loading
- Default.cshtml - The default content template that demonstrates content area layout and metadata handling
These templates serve as the de-facto recommended structure and demonstrate best practices for:
- Using
@Href()for internal navigation links with BasePath handling - Using
@Asset()for asset references with automatic cache busting - Handling content metadata with
@Model.GetMeta() - Rendering content with
@Raw(Model.Body) - Working with the
@Model.Headerscollection for table of contents
Template Properties and Methods
Available to all templates are the following functions:
@Raw("<some string>")- the
Rawmethod will avoid HTML encoding content (which is the default).
- the
@Model.Body- This is the main HTML content being rendered in this template. You should output this through the
Rawmethod since it will likely contain HTML.
- This is the main HTML content being rendered in this template. You should output this through the
@Model.GetMeta("title")- with this you can access any value in the front matter (for example,
title).
- with this you can access any value in the front matter (for example,
@await PartialAsync("TemplateName", Model)- This lets you render sub templates.
- the template name parameter will match the razor file name ... so in the example above, it would be looking for a file named
Templates/TemplateName.cshtml - the second parameter is the model to pass to the template (typically
Modelto pass the current model). - Note: Template names are relative to the
templates_folder, so you reference them without thetemplates/prefix. For example, use"TranslationWarning"not"templates/TranslationWarning"(though the latter will work as a fallback).
@await PartialAsync("path")- Smart routing based on file extension:
.cshtmlextension → renders as a Razor template with currentModel.mdextension → renders as markdown file (can define its own template, defaults toDefault, noSiteFrame.cshtml)- No extension → checks if a template exists, otherwise treats as markdown file path
- Examples:
@await PartialAsync("topNav")→ renders template iftopNav.cshtmlexists@await PartialAsync("source/menu.md")→ renders markdown file@await PartialAsync("template/nav.cshtml")→ explicitly renders template
- Smart routing based on file extension:
@Model.Headerscollection- Each
h1-h6will be parsed and added to this headers collection. - Every
Headerobject has the following properties:- Level: the numeric value of the header
- Value: the text
- Slug: a slug version of the
Value, which corresponds to an anchor added to the HTML before the header.
- Each
URL and Asset Methods
For generating URLs and asset references in your templates:
@Href("/path/to/page")- Prefixes a site-relative URL with the configured BasePath
- Use this for internal site navigation links
- Example:
<a href="@Href("/about")">About</a>
@Asset("/path/to/asset.css")- Builds an asset URL with BasePath and automatic cache-busting version
- Combines both BasePath application and version parameter addition
- Important: Do not manually append query parameters to Asset() results, as this will conflict with the automatic version parameter
- Example:
<link href="@Asset("/css/main.css")" rel="stylesheet" />
URL Method Examples
<!-- Navigation links using Href -->
<nav>
<a href="@Href("/")">Home</a>
<a href="@Href("/docs")">Documentation</a>
<a href="@Href("/about")">About</a>
</nav>
<!-- Asset references using Asset (preferred for CSS/JS) -->
<link href="@Asset("/css/main.css")" rel="stylesheet" />
<script src="@Asset("/js/app.js")"></script>
<!-- Manual approach using Href + WithVersion for more control -->
<link href="@WithVersion(Href("/css/main.css"))" rel="stylesheet" />
⚠️ Cache Busting Conflict Warning: When using Asset(), do not manually append query parameters or call WithVersion() on the result, as Asset() already includes cache busting. This would result in malformed URLs like /basePath/assets/script/samples?v=123/mysample.js.
Cache Busting Methods
For managing asset versioning and cache invalidation:
@WithVersion("/path/to/asset.css")- Adds a version parameter to asset URLs for cache busting (e.g.,
/path/to/asset.css?v=20231215123456) - Only adds the version parameter when cache busting is enabled in configuration
- Properly handles existing query parameters by using
&instead of?when appropriate
- Adds a version parameter to asset URLs for cache busting (e.g.,
@BuildVersion- Gets the current build version string, or
nullif cache busting is disabled - Useful for displaying version information or custom cache busting logic
- Gets the current build version string, or
Cache Busting Example
<!-- Recommended approach using WithVersion -->
<link href="@WithVersion("/assets/main.css")" rel="stylesheet" />
<script src="@WithVersion("/assets/site.js")"></script>
<!-- Manual approach using BuildVersion -->
@if (BuildVersion != null)
{
<link href="/assets/main.css?v=@BuildVersion" rel="stylesheet" />
}
else
{
<link href="/assets/main.css" rel="stylesheet" />
}
Localized Partials & Templates
Chloroplast provides helper methods to simplify rendering locale-specific partials (both Markdown content files and Razor template partials) without hard-coding locale-specific logic in every .cshtml file.
These helpers keep your templates clean and make it easy for site authors to add localized variants by simply creating files that follow a naming convention (e.g. menu.es.md).
Helper Methods
| Method | Purpose |
|---|---|
ResolveLocalizedContentPath(basePath, locale?) |
Returns the localized relative path if it exists (e.g. source/menu.es.md), otherwise the base path. Does not render anything. |
InsertLocaleBeforeExtension(path, locale) |
Utility that transforms file.md into file.es.md (used internally; available if you need custom logic). |
LocalizedMarkdownPartialAsync(basePath, locale?) |
Renders a localized Markdown content partial, falling back to the base file. |
LocalizedTemplatePartialAsync<TModel>(baseName, model, locale?, fallbackToBase=true) |
Renders a Razor template partial, trying baseName.{locale} first when locale != default, then falling back. |
Usage Examples
Render a localized menu Markdown file (tries source/menu.<locale>.md first):
@await LocalizedMarkdownPartialAsync("source/menu.md")
Render a localized Razor partial (e.g. TranslationWarning.cshtml + optional TranslationWarning.es.cshtml):
@(await LocalizedTemplatePartialAsync("TranslationWarning", Model))
Explicitly target a different locale (useful for language switchers or preview UIs):
@* Show how this page's menu would look in Spanish *@
@await LocalizedMarkdownPartialAsync("source/menu.md", locale: "es")
File Naming Conventions
For a base file:
source/menu.md
Add a localized variant by inserting the locale before the extension:
source/menu.es.md
source/menu.fr.md
For Razor partials (no extension required in calls):
TranslationWarning.cshtml
TranslationWarning.es.cshtml
TranslationWarning.fr.cshtml
Fallback Behavior
- Non-default locale requested → framework looks for localized variant.
- If localized file exists → it is rendered.
- If missing → base file is rendered (graceful fallback, no exception).
- You can force an exception instead of fallback with
fallbackToBase: falseonLocalizedTemplatePartialAsync.
When to Use Which
| Situation | Use |
|---|---|
| Rendering a Markdown content fragment (e.g. a sidebar, footer) | LocalizedMarkdownPartialAsync |
| Rendering a Razor partial (dynamic logic, custom model) | LocalizedTemplatePartialAsync |
| Just need the resolved path for custom logic (e.g. building a list) | ResolveLocalizedContentPath |
Migrating Existing Logic
If you previously had manual logic like:
@{
var baseMenu = "source/menu.md";
var localized = CurrentLocale != SiteConfig.DefaultLocale ? $"source/menu.{CurrentLocale}.md" : baseMenu;
// ... existence checks ...
}
@await PartialAsync(menuPathToUse)
Replace it with:
@await LocalizedMarkdownPartialAsync("source/menu.md")
And for localized warnings / notices:
@(await LocalizedTemplatePartialAsync("TranslationWarning", Model))
Customization
You can still build higher-level abstractions on top of these methods (e.g. a RenderSidebar() helper) in a derived template base class. The provided helpers deliberately avoid assumptions about specific file names beyond the locale insertion convention.
Edge Cases & Notes
- If a file has no extension (e.g.
fragment), a locale variant becomesfragment.es. InsertLocaleBeforeExtensiononly modifies the final segment; nested paths are preserved.- The existence check uses the configured site
rootand honors directory separators on the current platform. - These helpers do not alter URL localization—they strictly manage which source content file is rendered.
With these helpers, you can keep templates concise while enabling contributors to add localized content simply by creating appropriately named files.
Partial Metadata Inheritance
Partials automatically inherit metadata from their parent content page or layout, enabling context-aware components like active navigation states, breadcrumbs, and conditional widgets.
How It Works
When a partial is rendered, Chloroplast merges the parent's metadata with the partial's metadata:
- The partial first inherits all metadata from its parent (content page, layout, or outer partial)
- The partial's own front matter is then layered on top
- Child metadata values override parent values for the same keys
- This process works recursively through nested partials
Use Cases
Partials can access parent page metadata to:
- Mark the active navigation item based on the current page
- Display context-aware breadcrumbs
- Show/hide elements based on page-level flags
- Pass configuration from content pages to shared widgets
Example: Active Navigation State
A common use case is highlighting the active page in a navigation menu. With metadata inheritance, the nav partial can check the parent page's activeNav value:
Content Page (index.md):
---
title: Home
activeNav: home
---
# Welcome Home
Nav Partial (source/nav.md):
---
template: nav
---
Navigation content here
Nav Template (templates/nav.cshtml):
@inherits Chloroplast.Core.Rendering.ChloroplastTemplateBase<Chloroplast.Core.Rendering.RenderedContent>
<nav>
@{
// Access parent page's activeNav metadata
var activeNav = Model.GetMeta("activeNav");
}
<a href="@Href("/")" class="@(activeNav == "home" ? "active" : "")">Home</a>
<a href="@Href("/about")" class="@(activeNav == "about" ? "active" : "")">About</a>
<a href="@Href("/contact")" class="@(activeNav == "contact" ? "active" : "")">Contact</a>
</nav>
Usage in Layout (templates/Default.cshtml):
@inherits Chloroplast.Core.Rendering.ChloroplastTemplateBase<Chloroplast.Core.Rendering.RenderedContent>
<div class="page">
@await LocalizedMarkdownPartialAsync("source/nav.md")
<main>
<h1>@Model.GetMeta("title")</h1>
@Raw(Model.Body)
</main>
</div>
Note: See Localized Partials & Templates for more details on when to use LocalizedMarkdownPartialAsync versus PartialAsync.
The nav partial will render with "Home" marked as active because it inherits the activeNav: home metadata from index.md.
Metadata Override Behavior
Child metadata always overrides parent metadata for the same key:
Parent Page:
---
title: Parent Title
activeNav: home
sharedKey: parentValue
---
Child Partial:
---
template: widget
sharedKey: childValue
newKey: childOnlyValue
---
Result: The partial sees:
title: "Parent Title" (inherited)activeNav: "home" (inherited)sharedKey: "childValue" (child overrides parent)newKey: "childOnlyValue" (child-only)
Nested Partials
Metadata inheritance works through multiple levels. When rendering nested partials, each level sees the accumulated metadata from all parent levels, with child values overriding parent values at each level.
Best Practices
Use Descriptive Keys: Choose metadata key names that clearly indicate their purpose (e.g.,
activeNav,breadcrumbSection,showSidebar)Document Expectations: If your partial requires specific parent metadata, document it in comments:
@* This partial expects parent metadata: activeNav (string) *@Provide Defaults: Always check for metadata existence and provide sensible defaults:
@{ var activeNav = Model.GetMeta("activeNav"); if (string.IsNullOrEmpty(activeNav)) activeNav = "home"; }Avoid Conflicts: Be mindful of common key names that might conflict between partials and pages
Test Inheritance: Create test pages with various metadata combinations to ensure partials behave correctly
Backward Compatibility
Existing templates and partials work unchanged:
- Partials without parent metadata work as before
- Partials that don't reference parent metadata are unaffected
- No code changes required to existing templates
Technical Details
For developers extending Chloroplast:
- Metadata merging happens in
RenderedContent.MergeMetadata()usingConfigurationBuilder - The merge occurs in
ChloroplastTemplateBase.RenderMarkdownPartialAsync()before template rendering - Razor template partials called with
PartialAsync(templateName, Model)already pass the parent Model and inherit metadata naturally - Only markdown partials with their own front matter need special handling for metadata inheritance