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 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-static

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

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

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

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

9. 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-static

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

13. 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.html

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

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