Skip to content

ZarehD/AspNetStatic

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AspNetStatic

Platform Support: ASP.NET Core 6.0+ License: Apache 2     Build-And-Test

Transform ASP.NET Core into a Static Site Generator

Okay, so you want to create a static website. After doing some research, you learn that all the cool kids are using tools like Jekyll, Hugo, Gatsby, or Statiq. But what you also learn is that all of these tools require you to learn an entirely new way of constructing sites and pages. And then it occurs to you, I already know how to use ASP.NET Core to create websites, so why do I need to learn & use a whole other stack just for SSG? Isn't there a better way that lets me use the tools and skills I already have?

Well, now there is!

Create a static site using ASP.NET Core

AspNetStatic lets you generate a static website with the same ASP.NET Core tools you love and use every day. Just add this module and a bit of configuration, and BAM!, you have yourself a static site generator.

But wait, there's more!

AspNetStatic can also be used in a mixed mode configuration where some of the pages in your site are static html files (generated with the same _layout & page layers that define the look & feel of the rest of your site), while others remain dynamically generated per request. See Partial Static Site under Scenarios section below.

Oh, and one more thing!

AspNetStatic now works with Blazor websites, thanks to the new Blazor SSR capability in ASP.NET Core 8.

Blazor pages must not rely on any client-side (JS, WASM) behavior for rendering, or behaviors like showing a placeholder (e.g. a spinner) before rendering the actual content. The rule-of-thumb (for any technology you use with AspNetStatic) is that as long as the content has completed rendering by the time AspNetStatic receives it (via its HttpClient request), it will work fine.

No Frameworks. No Engines. No Opinions!

Build your ASP.NET site the way you've always done. AspNetStatic doesn't have any opinions about how you should build your server-rendered site. AspNetStatic is not a framework. It's not a CMS. There's no blog engine. It has no templating system. AspNetStatic does just one thing, create static files for selected routes in your ASP.NET Core app. That means you can use whatever framework, component, package, or architectural style you like. Want to use a blog engine? No problem. Want to use a CMS? No problem. Want to create a documentation site using a markdown processor to render page content? No problem! AspNetStatic doesn't care; it will create optimized static files no matter how the content is produced by the server.


Great. So how do I use it?

It's a piece of cake.

  1. Add the Nuget Package to your ASP.NET Core web app project
    dotnet add package AspNetStatic
    
  2. Specify the routes for which you want static files to be generated
    • Create an instance of StaticResourcesInfoProvider (or an object that derives from StaticResourcesInfoProviderBase or implements the IStaticResourcesInfoProvider interface)
    • Populate the PageResources and/or OtherResources collections
      • Set required Route property of each item
      • Set other properties as appropriate
    • Set other IStaticResourcesInfoProvider attributes as appropriate
    • Register it in the DI container
    builder.Services.AddSingleton<IStaticResourcesInfoProvider>(
       new StaticResourcesInfoProvider()
         .AddAllProjectRazorPages(builder.Environment) // from AspNetStaticContrib project
         .AddAllWebRootContent(builder.Environment));  // from AspNetStaticContrib project
    
    -- OR --
    
    builder.Services.AddSingleton<IStaticResourcesInfoProvider>(
      new StaticResourcesInfoProvider(
        new []
        {
          new PageResource("/"),
          new PageResource("/privacy"),
          new PageResource("/blog/articles/posts/1") { OutFile = "blog/post-1.html" },
          new PageResource("/blog/articles/posts/2") { OutFile = "blog/post-2-dark.html", Query = "?theme=dark" },
          new CssResource("/bootstrap/bootstrap.min.css") { OptimizerType = OptimizerType.None },
          new CssResource("/site.css"),
          new JsResource("/site.js"),
          new BinResource("/favicon.png")
        }));
  3. Add a call to the AspNetStatic module in the app startup
    ...
    app.MapRazorPages();
    ...
    app.GenerateStaticContent(@"C:\SSG-Output-Folder");
    app.Run();
  4. Run your app
    dotnet run
    

AspNetStatic is the EASY button for doing SSG with ASP.NET

You can use AspNetStatic in traditional SSG mode (generate files and exit the app), or in a 'partial-static site' mode. There is also an option to periodically regenerate the static content while your app is running. See the Scenarios section below for details.


Routes

Keep the following in mind when specifying routes in the IStaticResourcesInfoProvider.PageResources collection.

  • Routes must exclude the site's base URI (e.g. http:://localhost:5000, https://www.example.com)
  • As a rule, don't specify an 'index' page name; instead, opt for a route with a terminating slash (/ instead of /index).
  • You can directly specify the pathname of the file to be generated for routes you add to the PageResources collection (see OutFile property). The only requirement is that the specified path be relative to the destination root folder. If you do not specify a value for OutFile, the pathname for the generated file will be determined as demonstrated below.
  • You can specify route parameters for routes you add to the PageResources collection. The route parameters are treated as part of the route, and are used in constructing the output file pathname.
  • You can specify a query string for routes you add to the PageResources collection (see Query property). You can specify the same Route with different Query values, but you will need to specify a unique OutFile value for each instance of that route.
  • You can skip content optimization1 or choose a specific optimizer type for routes you add to the PageResources collection (see OptimizerType property). The default optimizer type setting, OptimizerType.Auto, automatically selects the appropriate optimizer.
  • You can set the encoding for content written to output files for routes you add to the PageResources collection (see OutputEncoding property). Default is UTF8.

NOTE: All of the above also applies to routes for CSS, JavaScript, and binary (e.g. image) files specified in the OtherResources collection property.

1: Content optimization options apply only when content optimization is enabled. Please see the Content Optimization section below for details.

Routes vs. Generated Static Files (page resources)

Assumes the following:

  • Resource Type: PageResource
  • Destination root: "C:\MySite"
  • OutFile: null, empty, or whitespace
Url
(route + query)
Always Default
false
Always Default
true
/ C:\MySite\index.html C:\MySite\index.html
/index C:\MySite\index.html C:\MySite\index.html
/index/ C:\MySite\index\index.html C:\MySite\index\index.html
/page C:\MySite\page.html C:\MySite\page\index.html
/page/ C:\MySite\page\index.html C:\MySite\page\index.html
/page/123 C:\MySite\page\123.html C:\MySite\page\123\index.html
/page/123/ C:\MySite\page\123\index.html C:\MySite\page\123\index.html
/page/123?p1=v1 C:\MySite\page\123.html C:\MySite\page\123\index.html
/page/123/?p1=v1 C:\MySite\page\123\index.html C:\MySite\page\123\index.html
/blog/articles/ C:\MySite\blog\articles/index.html C:\MySite\blog\articles\index.html
/blog/articles/post1 C:\MySite\blog\articles\post1.html C:\MySite\blog\articles\post1\index.html

Routes vs. Generated Static Files (non-page resources)

Assumes the following:

  • Resource Type: CssResource, JsResource, or BinResource
  • Destination root: "C:\MySite"
  • OutFile: null, empty, or whitespace
  • AlwaysDefaultFile not applicable.
Url
(route + query)
Generated File
/file.css C:\MySite\file.css
/folder/file.css C:\MySite\folder\file.css
/file.css?v=123 C:\MySite\file.css
/file C:\MySite\file.css (CssResource)
/file/ C:\MySite\file.css (CssResource)
/file.js C:\MySite\file.js
/folder/file.js C:\MySite\folder\file.js
/file.js?v=123 C:\MySite\file.js
/file C:\MySite\file.js (JsResource)
/file/ C:\MySite\file.js (JsResource)
/file.png C:\MySite\file.png
/folder/file.png C:\MySite\folder\file.png
/file.png?v=123 C:\MySite\file.png
/file C:\MySite\file.bin (BinResource)
/file/ C:\MySite\file.bin (BinResource)

Fallback Middleware: Routes vs. Served Content

Assumes the following:

  • OutFile: null, empty, or whitespace
  • Applicable only to PageResource items.
Url
(route + query)
Is Static Route: false

Is Static Route: true
Always Default: false
Is Static Route: true
Always Default: true
/ /index.cshtml /index.html /index.html
/index /index.cshtml /index.html /index.html
/index/ /index/index.cshtml /index/index.html /index/index.html
/page /page.cshtml /page.html /page/index.html
/page/ /page/index.cshtml /page/index.html /page/index.html
/page/123 /page.cshtml /page/123.html /page/123/index.html
/page/123/ /page.cshtml /page/123/index.html /page/123/index.html
/page/123?p1=v1 /page.cshtml /page/123.html /page/123/index.html
/page/123/?p1=v1 /page.cshtml /page/123/index.html /page/123/index.html
/blog/articles/ /blog/articles/index.cshtml /blog/articles/index.html /blog/articles/index.html
/blog/articles/post1 /blog/articles/post1.cshtml /blog/articles/post1.html /blog/articles/post1/index..html

The same rules apply when links in static files are updated to refer to other generated static pages.

IMPORTANT NOTE: In ASP.NET Core, UrlHelper (and the asp-* tag helpers) generate link URIs based on the routing configuration of your app, so if you're using them, be sure to specify an appropriate value for alwaysDefaultFile, as shown below. (NOTE: Specify the same value if/when configuring the fallback middleware).

// Sample routes: /, /index, and /page
//-------------------------------------

// generated links: /, /index, and /page
builder.Services.AddRouting(
  options =>
  { // default configuration in ASP.NET Core
    options.AppendTrailingSlash = false;
  });
...
// fallback static pages: /index.html, /index.html, and /page.html
builder.Services.AddStaticPageFallback(
  cfg =>
  {
    cfg.AlwaysDefaultFile = false;
  });
...
// generated static pages: /index.html, /index.html, and /page.html
app.GenerateStaticContent(
  alwaysDefaultFile: false);

-- OR --

// generated links: /, /index/, and /page/
builder.Services.AddRouting(
  options =>
  {
    options.AppendTrailingSlash = true;
  });
...
// fallback static pages: /index.html, /index/index.html, and /page/index.html
builder.Services.AddStaticPageFallback(
  cfg =>
  {
    cfg.AlwaysDefaultFile = true;
  });
...
// generated static pages: /index.html, /index/index.html, and /page/index.html
app.GenerateStaticContent(
  alwaysDefaultFile: true);

Scenarios

In all scenarios, ensure that routes for static content are unincumbered by authentication or authorization requirements.

Static Site Generation (Standalone SSG)

In this scenario, you want to generate a completely static website (to host on Netlify or Azure/AWS storage, for instance). Once the static pages are generated, you will take the files in the destination folder and xcopy deploy them to your web host.

Sample Configuration 1:

  • Specify any accessible folder as the destination-root for the generated static files.
  • Generate a default file only for routes ending with a slash.
  • Update the href attribute for <a> and <area> tags that refer to static pages (e.g. /page to /page.html).
    app.GenerateStaticContent(
      "../SSG_Output",
      exitWhenDone: true,
      alwaysDefaultFile: false,
      dontUpdateLinks: false);

Sample Configuration 2:

  • Specify any accessible folder as the destination-root for the generated static files.
  • Generate a default file for all routes (e.g. /page and /page/ to /page/index.html).
  • Don't update the href attribute for <a> and <area> tags that refer to static pages.
  • Use your web server's features to re-route requests (e.g. /page/ or /page/index to /page/index.html).
    // true when app is executed with one of the marker args, such as SSG.
    //  dotnet run -- ssg
    var exitWhenDone = args.HasExitWhenDoneArg();
    
    app.GenerateStaticContent(
      @"C:\path\to\destination\root\folder",
      exitWhenDone: exitWhenDone,
      alwaysDefaultFile: true,
      dontUpdateLinks: true);

If you want to omit static-file generation while you're still developing the site, you can configure a launchSettings profile for SSG mode operation. To enable this, you would surround the GenerateStaticContent() call with an IF gate.

"profiles": {
  "SSG": {
      "commandName": "Project",
      "commandLineArgs": "ssg",
      "launchBrowser": false,
      "applicationUrl": "https://localhost:5000",
  }
}

Then, in the startup code (Program.cs)

if (args.HasExitWhenDoneArg())
{
  app.GenerateStaticContent(
    @"path\to\destination\root\folder",
    exitWhenDone: true,
  );
}

Now you can use the SSG profile to launch your app in SSG mode (to generate static content, then exit), and a differrent launch profile while you're in development mode, editing the site content. (The BlazorSSG sample demonstrates this approach.)

Partial Static Site

In this scenario, you want some of the pages in your ASP.NET Core app to be static, but still want other routes to be served as dynamic content per request (e.g. pages/views, JSON API's, etc.). When the app runs, static (.html) files will be generated for routes you specify. The website will then serve these static files for the specified routes, and dynamic content (as usual) for others.

While static files are being generated, requests to routes for which a static file has not yet been generated will be served as dynamicly generated content (using the source .cshtml page). Once the static file for that route has been generated, it will be used to satisfy subsequent requests.

The configuration options are generally the same as for a standalone static site, except the following differences:

  • The destination root folder must be app.Environment.WebRoot (i.e. wwwroot).
  • You must do one of the following (can do both):
    • Use the AspNetStatic fallback middleware.
    • Allow links in generated static files to be updated (href of <a> and <area> tags).
  • Do not exit the app after static files are generated (obviously, right?)

Like this:

...
builder.Services.AddStaticPageFallback();
...
app.UseStaticPageFallback();     // re-route to the static file (page resources only)
app.UseStaticFiles();
...
app.UseRouting();
...
app.Map...();
...
app.GenerateStaticContent(
  app.Environment.WebRoot,       // must specify wwwroot
  exitWhenDone: false,           // don't exit after generating static files
  alwaysDefaultFile: true/false,
  dontUpdateLinks: false);       // update links so they refer to static files
...
app.Run();

NOTE: The fallback middleware only re-routes requests for routes that match entries in the PageResources collection, and only if a generated static file exists for that route.

Periodic Regeneration

If the data used in the content of static files changes while the app is running, you can configure periodic regeneration by specifying a value for the regenerationInterval parameter in the GenerateStaticContent() call. This will result in static files being generated when the app starts, and then periodically based on the specified interval.

app.GenerateStaticContent(
  ...
  exitWhenDone: false,
  regenerationInterval: TimeSpan.FromHours(2) // re-generate static files every 2 hours
);

Content Optimization

AspNetStatic automatically minifies HTML content (and any embedded CSS or Javascript) in generated static files; configuration is not required. To disable this feature, however, you can specify true for the dontOptimizeContent parameter.

app.GenerateStaticContent(
  ...
  dontOptimizeContent: true);

Content optimization does not apply to binary resource types (BinResource entries), but is enabled by default (OptimizerType.Auto) for all other resource types (PageResource, CssResource, and JsResource entries).

Configuration

To override the default minification settings used by AspNetStatic, register the appropriate objects in the DI container, as shown below.

AspNetStatic uses the excellent WebMarkupMin package to implement the minification feature. For details about the configuration settings, please consult the WebMarkupMin documentation.

Content optimization can be customized in one of two ways:

  1. Create and register an object that implements IOptimizerSelector. In addition to specifying custom optimizer configurations, this option allows you to implement your own custom logic for selecting the optimizer to use for a resource.

    public class MyOptimizerSelector : IOptimizerSelector { ... }
    ...
    builder.Services.AddSingleton(sp => new MyOptimizerSelector( ... ));
  2. Create and register individual settings objects which internally feed into a default IOptimizerSelector implementation.

    • HTML: To configure the HTML minifier, register as a singleton a configured instance of HtmlMinificationSettings:

      using WebMarkupMin.Core;
      builder.Services.AddSingleton(
        sp => new HtmlMinificationSettings()
        {
          ...
        });
    • XHTML: To configure the XHTML minifier, register as a singleton a configured instance of XhtmlMinificationSettings:

      using WebMarkupMin.Core;
      builder.Services.AddSingleton(
        sp => new XhtmlMinificationSettings()
        {
          ...
        });
    • XML: To configure the XML minifier, register as a singleton a configured instance of XmlMinificationSettings:

      using WebMarkupMin.Core;
      builder.Services.AddSingleton(
        sp => new XmlMinificationSettings()
        {
          ...
        });
    • CSS: To configure the CSS minifier, register as a singleton an object that implements the ICssMinifier interface:

      using WebMarkupMin.Core;
      builder.Services.AddSingleton<ICssMinifier>(
        sp => new YuiCssMinifier(...));
    • Javascript: To configure the Javascript minifier, register as a singleton an object that implements the IJsMinifier interface:

      using WebMarkupMin.Core;
      builder.Services.AddSingleton<IJsMinifier>(
        sp => new YuiJsMinifier(...));
    • Binary Content (BinResource): To configure a binary resource optimizer, register as a singleton an object that implements the IBinOptimizer interface:

      builder.Services.AddSingleton<IBinOptimizer, MyBinOptimizer>();

License

Apache 2.0


If you like this project, or find it useful, please give it a star. Thank you.