Static Site Generation
Heimdall can render explicitly registered routes into static files using the same server rendering code and dependency injection container as the running app.
Use it when a page can be built ahead of time but you still want to compose that page with FluentHtml, layouts, services, assets, sitemap, robots.txt, and normal ASP.NET Core startup.
1. Start With the Template
The fastest way to begin is the Heimdall SSG template. It creates an ASP.NET Core app with static generation already wired, sample fluent, MVC, markdown, and hybrid pages, build-time generation, copied assets, sitemap, robots.txt, and a shared path-base aware layout.
dotnet new install HeimdallFramework.Templates.SsgApp
dotnet new heimdall-ssg -n MyHeimdallDocs
cd MyHeimdallDocs
dotnet run
# Generate static output
dotnet build
dotnet run -- --heimdall-generate-static2. The Basic Model
Static generation is opt-in. The app registers the pages it wants generated, then the same executable can either run normally or exit after writing static output when the generation command is provided.
Normal app run:
dotnet run
Static generation run:
dotnet run -- --heimdall-generate-static
Output:
dist/
index.html
docs/index.html
404.html
heimdall.static.manifest.json3. Register Generation
Register static generation before builder.Build. Pages are added through the returned static site builder.
builder.Services
.AddHeimdallStaticSiteGeneration(options =>
{
options.OutputPath = "dist";
options.CleanOutputPath = true;
options.CopyWebRootAssets = true;
options.CopyStaticWebAssets = true;
options.UseSitemap("https://example.com");
options.UseRobotsTxt();
})
.WithStaticPage("/", () =>
MainLayout.Render(HomePage.Render(), "Home"))
.WithStaticPage("/docs", ctx =>
MainLayout.Render(DocsPage.Render(ctx), "Docs"))
.WithNotFoundPage(() =>
MainLayout.Render(NotFoundPage.Render(), "Not Found"));4. Final Run Call
Use RunWithHeimdallStaticSiteGenerationAsync as the final run call when the executable should support static generation.
var app = builder.Build();
app.MapStaticAssets();
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHeimdall();
await app.RunWithHeimdallStaticSiteGenerationAsync(args);5. Page Context
A page can accept HeimdallStaticPageContext. Each page render gets a fresh DI scope, route metadata, output paths, path base, and cancellation token.
.WithStaticPage("/docs", async ctx =>
{
var docs = ctx.GetRequiredService<IDocumentationRepository>();
var model = await docs.GetAllAsync(ctx.CancellationToken);
return DocsLayout.Render(
DocsPage.Render(model),
cssPath: ctx.ToSitePath("/css/site.css"));
})6. Route to File Mapping
Routes are written using common static hosting conventions. Extension routes are preserved, while directory-style routes receive an index.html file.
/ -> index.html
/about -> about/index.html
/docs/start -> docs/start/index.html
/feed.xml -> feed.xml
/404.html -> 404.html7. Path Base
Use a path base when the generated site is hosted under a subdirectory. Use ctx.ToSitePath for rooted links inside generated pages.
options.UsePathBase("/portal");
.WithStaticPage("/", ctx =>
FluentHtml.HtmlTag(html =>
{
html.Head(head =>
{
head.Link(link =>
{
link.Rel("stylesheet")
.Href(ctx.ToSitePath("/css/site.css"));
});
});
}))
// Result:
// /portal/css/site.css8. Assets
By default the generator copies physical web root files and static web assets exposed through ASP.NET Core. That keeps normal layout references working on static hosts.
Copied from wwwroot:
css/app.css
js/site.js
images/logo.png
Copied from static web assets:
_content/HeimdallFramework.Web/heimdall-bundle.min.js9. Manifest and Clean Output
Heimdall writes heimdall.static.manifest.json by default. The manifest records generated pages, copied assets, supplemental files, byte counts, and relative output paths. CleanOutputPath removes stale generated output before writing.
options.WriteManifest = true;
options.ManifestFileName = "heimdall.static.manifest.json";
options.CleanOutputPath = true;
// Disable manifest only when your deployment does not need it.
options.WriteManifest = false;10. Sitemap and Robots
UseSitemap generates sitemap.xml for normal HTML routes. UseRobotsTxt writes a permissive robots.txt by default and includes the sitemap URL when sitemap generation is enabled.
options.UseSitemap("https://example.com");
options.UseRobotsTxt();
// Custom robots.txt content
options.UseRobotsTxt(
"User-agent: *\n" +
"Allow: /\n" +
"Disallow: /admin/\n\n" +
"Sitemap: https://example.com/sitemap.xml\n");11. Manual Generation
The executable recognizes the default generation command and two shorter aliases.
dotnet run -- --heimdall-generate-static
dotnet run -- --generate-static
dotnet run -- generate-static12. Build and Publish Targets
The server package includes opt-in MSBuild targets that run the same generation command after a successful build or publish.
<PropertyGroup>
<GenerateHeimdallStaticSiteOnBuild>true</GenerateHeimdallStaticSiteOnBuild>
</PropertyGroup>
dotnet build -c Release \
-p:GenerateHeimdallStaticSiteOnBuild=true
dotnet publish -c Release \
-p:GenerateHeimdallStaticSiteOnPublish=true13. Targeting Web Root
The default output path is dist beneath the content root. Use UseWebRootPath when generated files should be written under the ASP.NET Core web root instead.
builder.Services.AddHeimdallStaticSiteGeneration(options =>
{
// Writes directly to IWebHostEnvironment.WebRootPath
options.UseWebRootPath();
// Or beneath wwwroot
options.UseWebRootPath("static-site");
});14. Serving from ASP.NET Core
If the app itself serves the generated output, use default files before static files so directory routes can resolve index.html.
app.MapStaticAssets();
app.UseDefaultFiles();
app.UseStaticFiles();
// /docs -> /docs/index.html15. Validation and Failure Behavior
The generator validates routes, prevents path traversal, detects duplicate output files, and can fail instead of overwriting existing files.
options.OverwriteExistingFiles = false;
Rejected routes include:
../escape
/docs?x=1
/docs#intro
Duplicate output examples:
/about
/about/index.html16. When to Use It
Reach for static generation when a route is public, cacheable, and can be rendered ahead of time. Keep dynamic Heimdall actions for user-specific or request-time UI.
Good fits:
- documentation
- marketing pages
- public landing pages
- generated feeds
- static fallback pages
Keep dynamic:
- authenticated dashboards
- per-user workflows
- mutable forms
- live SSE surfacesNext Steps
Static generation is mostly about deployment boundaries, asset paths, and deciding which routes should become files.