Skip to documentation
Heimdall Docs
Heimdall Docs

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 dedicated Heimdall Static Site Generator template. It creates an ASP.NET Core app with static generation already wired, sample FluentHtml, MVC, and markdown pages, build-time generation, copied assets, robots.txt, optional sitemap generation, and a shared path-base aware layout.

Shell
dotnet new install HeimdallFramework.Templates
dotnet new heimdall-ssg -n MyHeimdallDocs --output-mode artifact
cd MyHeimdallDocs
dotnet run

# Generate static output
dotnet build
dotnet run -- --heimdall-generate-static

2. Choose An Output Mode

The SSG template keeps generation explicit while letting you choose where the generated files go. Artifact mode writes to dist/ for deployment to a static host. Web-root mode writes to wwwroot/ so ASP.NET Core can serve the generated files. Both modes are static-only; the template does not add the Heimdall browser runtime or live content-invocation pipeline. Sitemap generation is opt-in through Site:BaseUrl so a fresh project does not publish placeholder URLs.

Text
# Deploy dist/ to a static host:
dotnet new heimdall-ssg -n MyStaticSite --output-mode artifact

# Let ASP.NET Core serve generated wwwroot files:
dotnet new heimdall-ssg -n MyHostedStaticSite --output-mode web-root

# Optional appsettings.json configuration:
{
  "Site": {
    "BaseUrl": "https://docs.example.com"
  }
}

3. 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.

Text
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.json

4. Register Generation

Register static generation before builder.Build. Pages are added through the returned static site builder.

Text
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"));

5. Final Run Call

Use RunWithHeimdallStaticSiteGenerationAsync as the final run call. The generated template serves normally when started without a generation argument and exits after writing files when generation is requested. A static-only template does not need app.UseHeimdall(); add that middleware only when the same application also hosts dynamic Heimdall invocations.

C#
var app = builder.Build();

app.MapStaticAssets();
app.UseDefaultFiles();
app.UseStaticFiles();

await app.RunWithHeimdallStaticSiteGenerationAsync(args);

6. Page Context

A page can accept HeimdallStaticPageContext. Each page render gets a fresh DI scope, route metadata, output paths, path base, and cancellation token.

C#
.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"));
})

7. HTML Return Types

Static pages accept any IHtmlContent implementation. Context-aware asynchronous pages can return Task<THtml> or ValueTask<THtml> when THtml implements IHtmlContent, including IHtmlAsyncContent implementations whose asynchronous writer is awaited before the file is written.

C#
.WithStaticPage("/feed", ctx =>
{
    var content = new HtmlContentBuilder();
    content.AppendHtml("<rss></rss>");
    return ValueTask.FromResult(content);
})

.WithStaticPage("/async", ctx =>
    ValueTask.FromResult<IHtmlAsyncContent>(
        new AsyncFeedContent()))

8. 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.

Text
/              -> index.html
/about         -> about/index.html
/docs/start    -> docs/start/index.html
/feed.xml      -> feed.xml
/404.html      -> 404.html

9. Path Base

Use a path base when the generated site is hosted under a subdirectory. Use ctx.ToSitePath for rooted links inside generated pages.

C#
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.css

10. 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.

C#
Copied from wwwroot:
css/app.css
js/site.js
images/logo.png

Copied from referenced static web assets when present:
_content/<package-name>/...

The SSG template itself does not reference
HeimdallFramework.Web, so it does not copy a
Heimdall browser bundle into the static site.

11. 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.

Text
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;

12. 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.

Text
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");

13. Manual Generation

The executable recognizes the default generation command and two shorter aliases.

Shell
dotnet run -- --heimdall-generate-static
dotnet run -- --generate-static
dotnet run -- generate-static

14. Build and Publish Targets

The server package includes opt-in MSBuild targets that run the same generation command after a successful build or publish.

HTML
<PropertyGroup>
  <GenerateHeimdallStaticSiteOnBuild>true</GenerateHeimdallStaticSiteOnBuild>
</PropertyGroup>

dotnet build -c Release \
  -p:GenerateHeimdallStaticSiteOnBuild=true

dotnet publish -c Release \
  -p:GenerateHeimdallStaticSiteOnPublish=true

15. 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.

Text
builder.Services.AddHeimdallStaticSiteGeneration(options =>
{
    // Writes directly to IWebHostEnvironment.WebRootPath
    options.UseWebRootPath();

    // Or beneath wwwroot
    options.UseWebRootPath("static-site");
});

16. 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.

C#
app.MapStaticAssets();
app.UseDefaultFiles();
app.UseStaticFiles();

// /docs -> /docs/index.html

17. Validation and Failure Behavior

The generator validates routes, prevents path traversal, detects duplicate output files, and can fail instead of overwriting existing files.

Text
options.OverwriteExistingFiles = false;

Rejected routes include:
../escape
/docs?x=1
/docs#intro

Duplicate output examples:
/about
/about/index.html

18. When to Use It

Reach for static generation when a route is public, cacheable, and can be rendered ahead of time. Keep dynamic Heimdall invocations for user-specific or request-time UI.

C#
Good fits:
- documentation
- marketing pages
- public landing pages
- generated feeds
- static fallback pages

Keep dynamic:
- authenticated dashboards
- per-user workflows
- mutable forms
- live SSE surfaces