<?xml version="1.0" encoding="UTF-8" standalone="no"?><rss version="2.0">
  <channel>
    <title>Rick Strahl's Web Log</title>
    <link>https://weblog.west-wind.com/</link>
    <image>
      <url>ImageUrl</url>
      <title>Rick Strahl's Weblog</title>
      <link>https://weblog.west-wind.com/</link>
    </image>
    <description>Wind, waves, code and everything in between</description>
    <copyright>(c) West Wind Technologies 2006-2026</copyright>
    <pubDate>2026-05-28T21:16:43.980549Z</pubDate>
    <lastBuildDate>2026-05-27T00:16:20.3303893Z</lastBuildDate>
    <generator>Rick Strahl's West Wind Weblog</generator>
    <xhtml:meta xmlns:xhtml="http://www.w3.org/1999/xhtml" content="noindex" name="robots"/><item>
      <title>Running ASP.NET Core Applications as a Subfolder Application</title>
      <description><![CDATA[<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/PostBanner.jpg" alt="Post Banner"></p>
<p>ASP.NET Core applications by default want to run in a root folder - and to be fair that's the 99% use case. But there are those occasional situations where you want to run a Web site in a sub folder rather than on the root of the Web site.</p>
<p>In this post I review what's required to run ASP.NET with a <code>PathBase</code> - which works with any Web server - and then specifically discuss how to set this up on IIS, which is a little more complicated than it should be.</p>
<blockquote>
<p>Although I discuss IIS specifically here for the physical deployment part since that's what I'm running on, the majority of this this content concerning the ASP.NET set up and modifications, applies to any Web server hosting when using sub folder mapping.</p>
</blockquote>
<p><a href="https://markdownmonster.west-wind.com?ut=weblog"  target="_blank"
	  title="Markdown Monster - Easy to use, yet powerfully productive Markdown Editing for Windows">
<img src="https://weblog.west-wind.com/images/sponsors/banner-example.png?v=1.2" class="da-content-image" />
</a></p>
<h2 id="why-would-i-need-to-run-out-of-a-subfolder">Why would I need to run out of a SubFolder?</h2>
<p>The specific scenario that I ran into was that recently I updated my old custom Blog engine, and decided to pull all my secondary blogs onto my own server from various blog publishing sites. I've always run my blog on my own hosted server and the main blog runs of its own sub-domain. But the other two blogs are small barely used product related blogs that ran on different hosting sites which I never used because they were terrible and it would be a better fit to just run them off the same site in <code>/blog/</code> folder.</p>
<p>With my recent site blog update - I finally moved the old WebForms app to .NET Core - and the much simplified deployment set up that comes with it, I decided to just run everything in house for these relatively low volume sites and get better, more consistent theming and a much easier publishing pipeline using Markdown Monster (using a new custom protocol - more on that in another post).</p>
<p>In any case the scenario here is that I have a root product site:</p>
<ul>
<li><a href="https://markdownmonster.west-wind.com">https://markdownmonster.west-wind.com</a></li>
</ul>
<p>and I now want to run the blog site out of a <code>/blog/</code> sub-folder, rather than as a root site. So:</p>
<ul>
<li><a href="https://markdownmonster.west-wind.com/blog">https://markdownmonster.west-wind.com/blog</a></li>
</ul>
<p>rather than a separate new root Web site like:</p>
<ul>
<li><a href="https://markdownmonsterblog.west-wind.com">https://markdownmonsterblog.west-wind.com</a></li>
</ul>
<p>(which is what I started with then aborted)</p>
<p>For SEO it's often beneficial to run everything on the same site which matters more for the low volume sites, but it's also cleaner and more consistent with other sub folders like <code>/docs</code> and <code>/support</code>. Running a sub folder site also requires less setup - you don't need yet another certificate and custom bindings to manage and so on. So using a subfolder is effectively more lightweight from a config perspective. On the other hand, using a subfolder site requires that more care is given how urls are created in the application as we'll see in a minute.</p>
<h2 id="creating-an-application-in-a-subfolder">Creating an Application in a Subfolder</h2>
<p>There are three parts to the process of running an application in a subfolder:</p>
<ul>
<li>Configuring ASP.NET for running from a subfolder</li>
<li>Fixing up links so no hardcoded <code>/</code> references are used</li>
<li>Configuring the Web Server for a subfolder application</li>
</ul>
<h3 id="setting-up-aspnet-for-running-with-a-subfolder">Setting up ASP.NET For running with a Subfolder</h3>
<p>Turns out setting up ASP.NET to run from a subfolder is pretty easy to do as there's a dedicated middleware to set up a custom <code>PathBase</code> as ASP.NET likes to call it.</p>
<p>In <code>program.cs</code> add this:</p>
<pre><code class="language-cs">var app = builder.build();
...
app.UsePathBase(&quot;/blog/&quot;);
</code></pre>
<p>This code goes into <code>program.cs</code> after the builder has created an <code>app</code> instance using the provided services. You'll want to do this near the top of the app middleware declarations to ensure the folder is respected all the way through the middleware pipeline - you'll want this before authentication,  static files and certainly before any routing middleware. I have it at the very top of the pipeline immediately after the builder has created the <code>app</code> instance.</p>
<p>I tend to parameterize the PathBase parameter with a configuration value, because I'm  duplicating the same application in multiple folders. So, in my Weblog application it looks like this:</p>
<pre><code class="language-csharp">// config from DI initialization or wlApp.Configuration static
if (!string.IsNullOrEmpty(config.VirtualPath) &amp;&amp; config.VirtualPath != &quot;/&quot;)
{
	app.UsePathBase($&quot;/{config.VirtualPath}/&quot;);
}
</code></pre>
<p>I have 3 blog sites - one of which runs as root and two of which run in <code>/blog/</code> subfolders. By parameterizing I can customize whether they run out of a subfolder or not without recompilation.</p>
<p>So what does <code>.AddPathBase()</code> actually do?</p>
<p>It's used to resolve Urls internally, using the path specified in <code>AddPathBase()</code>. Anytime ASP.NET creates a path dynamically for routes, uses <code>~/</code> in Views or Pages, or via <code>Url.Content()</code> or other <code>IUrlHelper</code> the path is automatically fixed up with the provided path base.</p>
<p>So instead of returning <code>/images/someimage.png</code> which you'd get for a root site, you get <code>/blog/images/someimage.png</code> for example.</p>
<p>If you use implicit routing, Url helper methods,  or you stick to using <code>~/</code> paths in your Views/Pages, ASP.NET does most of the heavy lifting for you,  without having to do anything else.</p>
<h4 id="fixing-up-root-paths-with--and-an-applicationbasepath">Fixing up Root Paths with <code>~/</code> and an ApplicationBasePath</h4>
<p>All this means is that you need to be more vigilant about how you <strong>root</strong> any explicitly referenced Urls both in View markup and in your application code. Code fixups should be minimized as much as possible.</p>
<p>For Views:  Rather than  using <code>&lt;img src=&quot;/images/someimage.png&quot; /&gt;</code> you should use <code>&lt;img src=&quot;~/images/someimage.png&quot; /&gt;</code> to ensure the appropriate path is used.</p>
<p>If you didn't do this - and let's be honest most of us don't - you can quickly find and replace all instances of hard coded root paths by doing a <strong>Find in Files Search</strong> (Ctrl-Shift-F) in your IDE and doing a search and replace for <code>=&quot;/</code> and replacing with <code>=&quot;~/</code> in all your View files. This should capture most scenarios in any physical files.</p>
<p>In code you can access the <code>IUrlHelper</code> interface in Razor views or injected into controllers or methods.</p>
<p><strong>In a Razor Page or View</strong></p>
<pre><code class="language-cs">var rootPath = Url.Content(&quot;~/images/someimage.png&quot;);
</code></pre>
<p><strong>In Application Code (.cs files)</strong></p>
<p>You can also inject <code>IUrlHelperFactory</code> (WTF Microsoft?) and then retrieve the <code>IUrlHelper</code> in a somewhat convoluted way that only an ivory tower architect could love:</p>
<pre><code class="language-csharp">public class MyService
{
    private readonly IUrlHelper _url;

	public MyService(
    			IUrlHelperFactory factory,
    			IActionContextAccessor actionContextAccessor)
	{
	    _url = factory.GetUrlHelper(
	        actionContextAccessor.ActionContext);
	}
	
	public string Resolve()
	{
	    return _url.Content(&quot;~/images/logo.png&quot;);
	}

}
</code></pre>
<p>If you don't want to deal with this and just have a couple of generic methods that work anywhere, the <a href="https://github.com/RickStrahl/Westwind.AspNetCore">Westwind.AspNetCore</a> package has a couple of generic helpers:</p>
<ul>
<li><a href="https://github.com/RickStrahl/Westwind.AspNetCore/blob/f244102fcc3b9fbed5f0d736bd0bb9cd2cd57799/Westwind.AspNetCore/Extensions/HttpContextExtensions.cs#L88">HttpContext.ResolveUrl()</a> extension method</li>
<li><a href="https://github.com/RickStrahl/Westwind.AspNetCore/blob/f244102fcc3b9fbed5f0d736bd0bb9cd2cd57799/Westwind.AspNetCore/Utilities/WebUtils.cs#L209">WebUtils.ResolveUrl()</a> - you provide a base path as a string and it resolves with that (works without any Context)</li>
</ul>
<h3 id="running-locally-with-a-subfolder">Running locally with a SubFolder</h3>
<p>If you create your app this way, and fix up Urls you run it locally using the Kestrel Web server with <code>dotnet run</code> or from your IDE you will see the site come up in a subfolder:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/SubfolderInBrowser.jpg" alt="Subfolder In Browser"></p>
<p>The url includes the <code>/docs/</code> subfolder:</p>
<pre><code class="language-text">https://localhost:5001/docs/
</code></pre>
<p>If you stuck to using <code>~/</code> root paths everything is bound to just work the same as if you were running from the root folder.</p>
<p>Note that you should also change your dev <code>launchSettings.json</code> to reflect the new <code>launchUrl</code> that includes the subfolder:</p>
<pre><code class="language-json">&quot;Westwind.Weblog.MarkdownMonster&quot;: {
  &quot;commandName&quot;: &quot;Project&quot;,
  &quot;launchBrowser&quot;: true,
  &quot;environmentVariables&quot;: {
    &quot;ASPNETCORE_ENVIRONMENT&quot;: &quot;Development&quot;
  },
  &quot;applicationUrl&quot;: &quot;https://localhost:5001&quot;,
  
  // THIS!
  &quot;launchUrl&quot;: &quot;https://localhost:5001/blog/&quot;
},
</code></pre>
<h3 id="configuring-iis-for-a-subfolder-application">Configuring IIS for a Subfolder Application</h3>
<p>While the <code>UsePathBase()</code> middleware handles the ASP.NET Core side seamlessly, IIS requires some extra work to create an <strong>Application</strong> under your root website.</p>
<p>IIS has Web Sites and Applications, which are very similar. Web Sites have extra configuration related to Host mappings and bindings, but otherwise Web Sites and Applications behave very similarly.</p>
<blockquote>
<h5 id="--one-aspnet-application-per-application-pool"><i class="fas fa-lightbulb" style="font-size: 1.1em"></i>  One ASP.NET Application per Application Pool</h5>
<p>IIS requires that each ASP.NET application uses <strong>its own dedicated ASP.NET Application Pool</strong>. Only a single ASP.NET Application can run inside of any given Application Pool, so unlike classic .NET sites, you can't share a single application pool for multiple Core apps.</p>
<p>It's possible to mix an ASP.NET Core app with a classic ASP.NET app or a static app in the same Application Pool, but you cannot have two or more ASP.NET Core Applications in an Application Pool. If you do only the first starts - the second one fails with a startup error.</p>
</blockquote>
<p>Here's what a folder based Application looks like in the IIS Manager:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/DedicatedApplicationPoolInIIS.png" alt="Dedicated Application Pool In IIS"></p>
<blockquote>
<h5 id="--application-vs-virtual-folder"><i class="fas fa-lightbulb" style="font-size: 1.1em"></i>  Application vs. Virtual Folder</h5>
<p>There are two options in IIS for a folder: Application or Virtual. In this post I'm only talking about Applications which are self contained apps that run in their own Application Pool.</p>
<p>You can also create a virtual which does not create or use a new Application Pool but simply provides a virtual folder mapping with the app running in the parents application scope. This might work in some scenarios where the parent site is not an ASP.NET Core app. A static site or an ASP.NET Classic site but I've not tried this out. What I describe here is the scenario of creating a new Application that is mapped to an Application Pool explicitly.</p>
</blockquote>
<p>Note that the physical folder doesn't have to live in the matching parent site folder  - it can point to any location on disk. However, in most cases I put the actual site content into the relative folder location in the parent Web site for consistency in finding it later 😄</p>
<p>For an Application you need to have a <strong>dedicated Application Pool</strong> for this application - or at least an AppPool that doesn't have another ASP.NET application in it. That AppPool should be configured for <strong>No Managed Code</strong>.</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/IISApplicationPool.png" alt="IIS Application Pool"></p>
<p>Make sure you use an identity (user) that matches the rights that you need for your application. My apps tend to have a few configuration related settings that get written out so I have to use an account that has some additional rights.</p>
<p>Once this is set up and you've published your app, IIS will handle the initial request routing and pass the correct path information to the ASP.NET Core module to give you the same behavior that you see on your local install.</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/RunningInIIs.png" alt="Running In IIS"></p>
<h2 id="recommendations">Recommendations</h2>
<p>If you're like me and you built this site with a Root Site in mind initially,  doing this switch to running a sub-folder - I'd highly recommend you take a breather 😄 and spend a bit of time testing locally with the subfolder.</p>
<p>There are bound to be lots of little edge cases that are hard to test and easy to miss when doing smoke testing.</p>
<p>I initially built the site for my main Weblog which runs on root, never giving any thought for running out of a sub-folder. Then when I created the other two sites I initially set them both up as root sites before realizing that I'd rather run them in sub folders.</p>
<p>I went through the initial steps of making the site work with subfolders, and at first glance everything worked. However I ran into lots of little edge cases with a couple of oddball links that used different quotes around links, and any links in Javascript code (see below). I managed to find most issue before uploading the site to production, but over the next day a whole bunch error log reports came in from things I had missed - mostly links.</p>
<p>So take the time to test your functionality, if not automated then manually excercising all the nooks and crannies of your site.</p>
<h3 id="summary-of-path-fixups">Summary of Path Fixups</h3>
<p>To summarize these are some of the things that you have to probably fix or at least check when going from Site to Folder based:</p>
<h3 id="all-cshtml-razor--pages">All .cshtml, .razor  Pages</h3>
<p>All view pages can automatically fix up pages that reference via &quot;~/&quot; paths. Maybe you were diligent and started doing this right way. I rarely ever do this right - I do some with <code>~/</code> and some not which is no better than not doing it all 😄.</p>
<p>Luckily fixing up View pages is pretty straight forward: You can do a simple search and replace:</p>
<ul>
<li>Select *.cshtml, *.razor etc.</li>
<li>Search for <code>=&quot;/</code></li>
<li>Replace with <code>=&quot;~/</code></li>
</ul>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/SearchInFiles.png" alt="Search In Files"></p>
<h3 id="code-fix-ups">Code Fix ups</h3>
<p>You can do something similar for your .NET code, but you have to be a lot more selective and you can't do a search and replace.</p>
<ul>
<li>Select *.cs, *.cshtml     (cshtml for code blocks that build Urls)</li>
<li>Search for <code>&quot;/</code></li>
</ul>
<p>You want to search code both in your CS files and any code snippets in your Views/Pages.</p>
<p>Hopefully there shouldn't be a lot of those in your code because generally hard coding Urls is a bad idea. Typically this only happens if Urls need to be built up based on a host of input parameters.</p>
<pre><code class="language-cs">var redirectUrl = &quot;/somedeeplink/in/the/site&quot;;
</code></pre>
<p>changed to:</p>
<pre><code class="language-cs">var redirectUrl = wlApp.Configuration.ApplicationBasePath + &quot;somedeeplink/in/the/site&quot;;
</code></pre>
<p>Again, this should be pretty rare but worth checking for. This search is likely to produce a lot of false positives that don't require any changes.</p>
<h3 id="javascript-code">Javascript Code</h3>
<p>Javascript code is little more tricky. If you have script code that's making server calls, you may have to fix up paths in your scripts. Scripts aren't executable code and the ASP.NET parser doesn't touch them as the files are served as static resources. It's also common that you provide Urls as strings.</p>
<p>It's a lesson I've learned a long time ago - almost every application that calls to the server requires a configurable value to for a base path to call the server. There are lots of ways to do this and in SPA applications I always have a config object to do this.</p>
<p>However, for traditional server based Web apps that have only a few isolated JavaScript callbacks to the server, there is no common entry point for scripts - they are just loaded randomly.</p>
<p>In this application I do this the brute force way by providing a base path in a global script variable that gets embedded into the <code>_Layout.cshtml</code> page. Since <code>_Layout.cshtml</code> touches every page this is as close as I get to a global client side entry point. You can also put this in a script file but you have to make sure it gets loaded early enough that all other scripts can see it.</p>
<p>The idea is that I create a global variable - or rather a global object with an embedded variable - that is accessible from any script.  The script is defined at the top of the page in the header so it's visible to any code that follows.</p>
<p>I use another helper class from <code>Westwind.AspNetCore</code> helper for this:</p>
<pre><code class="language-cs">@
{
	// at the top of _Layout.cshtml

	// creates an object with props for the value(s) below
	var scriptVars = new ScriptVariables(&quot;window.page&quot;);  
	scriptVars.Add(&quot;basePath&quot;, wlApp.Configuration.ApplicationBasePath);
}
&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head runat=&quot;server&quot;&gt;
    &lt;title&gt;@(ViewBag.Title ??  wlApp.Configuration.ApplicationName)&lt;/title&gt;
    &lt;script&gt;
        // expands into an object with props
        @scriptVars.ToHtmlString();
    &lt;/script&gt;
</code></pre>
<p><a href="https://github.com/RickStrahl/Westwind.AspNetCore/blob/f244102fcc3b9fbed5f0d736bd0bb9cd2cd57799/Westwind.AspNetCore/Utilities/ScriptVariables.cs#L100">ScriptVariables</a> is a helper class that makes it easy to create a Json safe object of values you want to embed into the page from your .NET code. You basically add variables which are then serialized - Javascript and Html safe into the document when you use <code>@scriptVars.ToHtmlString()</code>.</p>
<p>In this case it produces an object with a single variable, but typically I end up with a handful of 'global' page level properties that need to be passed through:</p>
<pre><code class="language-js">window.page = {
	basePath: &quot;https://markdownmonster.west-wind.com/blog/&quot;
};
</code></pre>
<p>You can decide whether you want to use just <code>/blog/</code> or the fully qualified path as I'm doing here.</p>
<p>I'm using:</p>
<pre><code class="language-cs">scriptVars.Add(&quot;basePath&quot;, wlApp.Configuration.ApplicationBasePath);
</code></pre>
<p>which is configured value that I store in the app to have quick and easy access to the full site url. In the case of this <code>_Layout.cshtml</code> page you could also use:</p>
<pre><code class="language-cs">scriptVars.Add(&quot;basePath&quot;, Url.Content(&quot;~/&quot;);
</code></pre>
<p>which produces the site relative <code>/blog/</code> base path.</p>
<p>Now any scripts on the page - both in the page itself or in any script files can look at <code>page.basePath</code> and use that to fix up any Urls as necessary:</p>
<pre><code class="language-js">deletePost = ()=&gt; {
	if (confirm('Are you sure you want to delete this post?')) {
		// THIS
		var url = page.basePath + &quot;posts/@post.Id&quot;;
		
		ajaxJson(url, null,
			(res) =&gt; { 
			    // AND THIS
			    location.href = page.basePath + &quot;posts&quot;;
			},
			(err) =&gt; {
			    alert(&quot;Error deleting post: &quot; + err.responseText);
			},
			{ HttpVerb: &quot;DELETE&quot; });                                                    
	}
}
</code></pre>
<p><a href="https://www.linqpad.net/?affiliate=4n4zaa6t"  target="_blank"
		  title="LinqPad - The Scratchpad for .NET Developers">
<img src="https://weblog.west-wind.com/images/sponsors/LinqPad-DisplayAd.jpg" class="da-content-image" />
</a></p>
<h2 id="summary">Summary</h2>
<p>Conversions like this are always more time consuming than you think - getting to 80% is easy. And just as you're padding yourself on the shoulder for an easy job you find all the edge cases that you didn't test for.</p>
<p>The basics mentioned above should get you through most of the conversion pretty quickly. It takes a little bit of time to set up, and I always kick myself for not doing things like using <code>~/</code> paths and explicit base path fixups for any coded paths right from the start.</p>
<p>Running Web sites out of a virtual folder is not all that common, but for many low impact sites I'm actually finding myself using them more often than I would have thought. When you need to do it, it's good to know that it's possible and not as complicated as I thought it might be.</p>
<p>Now that I've gone through it I suspect I will be more diligent in the future with new and old sites to use proper pathing from the get go even when it seems overkill and you think you'd never run any other way than out of a root site... we shall see.</p>
<div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;">
    <img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;">
    this post created and published with the 
    <a href="https://markdownmonster.west-wind.com" target="top">Markdown Monster Editor</a> 
</div>
]]></description>
      <link>https://weblog.west-wind.com/posts/2026/May/26/Running-ASPNET-Core-Applications-in-an-IIS-Subfolder-Application</link>
      <guid isPermaLink="false">qzzxfojpce9j</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/May/26/Running-ASPNET-Core-Applications-in-an-IIS-Subfolder-Application#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/May/26/Running-ASPNET-Core-Applications-in-an-IIS-Subfolder-Application</guid>
      <pubDate>Tue, 26 May 2026 14:16:20 GMT</pubDate>
      <abstract><![CDATA[While ASP.NET Core applications typically run from the root folder, some scenarios—such as hosting multiple blogs under a single domain—require running from a subfolder. This post explains how to configure ASP.NET Core with app.UsePathBase() for proper routing and ~/ path resolution, along with the IIS setup required for a dedicated Application Pool using "No Managed Code." It also covers key migration tips, including bulk updates for root-relative links and JavaScript adjustments to keep client-side functionality working in a subfolder environment.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/imageContent/2026/Running-ASP-NET-Core-Applications-in-an-IIS-Subfolder-Application/PostBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Getting the Client IP Address in ASP.NET Core</title>
      <description><![CDATA[<p><img src="https://weblog.west-wind.com/imageContent/2026/Getting-the-Client-IP-Address-in-ASP-NET-Core/ClientIpBanner.jpg" alt="Client Ip Banner"></p>
<p><a href="https://markdownmonster.west-wind.com?ut=weblog"  target="_blank"
	  title="Markdown Monster - Easy to use, yet powerfully productive Markdown Editing for Windows">
<img src="https://weblog.west-wind.com/images/sponsors/banner-example.png?v=1.2" class="da-content-image" />
</a></p>
<p>When I need to pick up the client IP Address in ASP.NET Core I always forget where to find the connection information.</p>
<p>It's simple enough:</p>
<pre><code class="language-cs">HttpContext?.Connection?.RemoteIpAddress
</code></pre>
<p>but I never remember to look on the context object as I expect it to be on the Request 😄.</p>
<p>It's also useful to remember that if requests are proxied, we need to return the <strong>forwarded IP address</strong>, rather than the proxy's IP Address. Finally, in most cases you'd likely want the ipv4 address rather than an IPv6 address.</p>
<p>Here's ready to use helper extension method for the <code>HttpRequest</code> class that makes this more easily accessible:</p>
<pre><code class="language-csharp">/// &lt;summary&gt;
/// Returns the client IPv4 Address for a request.
///
/// Checks proxy forwarding first, the actual ip
/// and returns null.
/// &lt;/summary&gt;
/// &lt;param name=&quot;request&quot;&gt;The HttpRequest instance.&lt;/param&gt;
/// &lt;param name=&quot;checkForProxy&quot;&gt;
/// Indicates whether to check for proxy headers.
/// 
/// Default returns the un-translated connection's IP Address
/// returned by the Web server.
///
/// When true, checks the Proxy forwarding headers 
/// `X-Forwarded-For`, `Forwarded` and `X-Real-IP`
/// in that order and returns the 1st valid IP address found.
/// &lt;/param&gt;
/// &lt;returns&gt;IP Address or null&lt;/returns&gt;
public static string GetClientIpAddress(this HttpRequest request, bool checkForProxy = false)
{
    if (request == null) return null;

    string ip = NormalizeIpAddress(request.HttpContext?.Connection?.RemoteIpAddress);
    if (!checkForProxy)
        return ip;

    string proxy = GetForwardedIpAddress(request.Headers[&quot;X-Forwarded-For&quot;].FirstOrDefault());
    if (!string.IsNullOrEmpty(proxy))
        return proxy;

    proxy = GetForwardedIpAddress(request.Headers[&quot;Forwarded&quot;].FirstOrDefault(), true);
    if (!string.IsNullOrEmpty(proxy))
        return proxy;

    proxy = GetForwardedIpAddress(request.Headers[&quot;X-Real-IP&quot;].FirstOrDefault());
    return proxy ?? ip;
}


/// &lt;summary&gt;
/// Handle various forwarding headers and their custom parsing
/// of multiple proxy chain values
/// &lt;/summary&gt;
/// &lt;param name=&quot;headerValue&quot;&gt;The value of the forwarding header.&lt;/param&gt;
/// &lt;param name=&quot;isForwardedHeader&quot;&gt;Indicates if the header is the `Forwarded` header.&lt;/param&gt;
/// &lt;returns&gt;The extracted IP address or null if none found.&lt;/returns&gt;
private static string GetForwardedIpAddress(string headerValue, bool isForwardedHeader = false)
{
    if (string.IsNullOrWhiteSpace(headerValue))
        return null;

    foreach (var value in headerValue.Split(','))
    {
        string candidate = value?.Trim();
        if (string.IsNullOrWhiteSpace(candidate))
            continue;

        if (isForwardedHeader)
        {
            candidate = candidate.Split(';')
                .Select(segment =&gt; segment?.Trim())
                .FirstOrDefault(segment =&gt; segment != null &amp;&amp; segment.StartsWith(&quot;for=&quot;, StringComparison.OrdinalIgnoreCase));

            if (string.IsNullOrWhiteSpace(candidate))
                continue;

            candidate = candidate.Substring(4).Trim();
        }

        candidate = candidate.Trim('&quot;');

        if (candidate.StartsWith(&quot;[&quot;, StringComparison.Ordinal) &amp;&amp; candidate.Contains(&quot;]&quot;, StringComparison.Ordinal))
            candidate = candidate.Substring(1, candidate.IndexOf(']') - 1);
        else if (candidate.Count(ch =&gt; ch == ':') == 1)
        {
            var parts = candidate.Split(':');
            if (parts.Length == 2 &amp;&amp; IPAddress.TryParse(parts[0], out _))
                candidate = parts[0];
        }


        if (string.Equals(candidate, &quot;unknown&quot;, StringComparison.OrdinalIgnoreCase))
            continue;

        if (IPAddress.TryParse(candidate, out var address))
            return NormalizeIpAddress(address);
    }

    return null;
}

/// &lt;summary&gt;
/// Return as IPv4 address if the address is an IPv4-mapped IPv6 address,
/// otherwise return the original address as a string.
/// &lt;/summary&gt;
/// &lt;param name=&quot;address&quot;&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
private static string NormalizeIpAddress(IPAddress address)
{
    if (address == null)
        return null;

    if (address.IsIPv4MappedToIPv6)
        address = address.MapToIPv4();

    return address.ToString();
}
</code></pre>
<p>The bulk of this code is related to the Proxy forwarding handling which is optional. If you know you're directly connected to the Internet, you can skip the proxy forwarding stuff - in fact it's a good idea to do this to avoid any spoofing from a client. The various forwarding headers provide multiple IP Addresses in the proxy chain and you essentially need to pick out the first IP address to get the original address.</p>
<p>You can also find this as part of the <code>HttpRequestExtensions</code> class in the <a href="https://github.com/RickStrahl/Westwind.AspNetCore/blob/f244102fcc3b9fbed5f0d736bd0bb9cd2cd57799/Westwind.AspNetCore/Extensions/HttpRequestExtensions.cs#L204"><code>Westwind.AspNetCore</code> package here</a> which provides a host of other small, but frequently used extensions.</p>
<h2 id="alternative-use-the-ip-forwarded-headers-middleware">Alternative: Use the IP Forwarded Headers Middleware</h2>
<p><small><em>thanks to @RichardD in the comments</em></small></p>
<p>If you know you are always running behind a proxy server, and you need the IP Address in all or most requests, you can run the Forwarded Headers Middleware which handles the above logic and simply populates the <code>HttpContext.Connection.RemoteIpAddress</code> making the process complete transparent. That certainly works, but depending on how you use IP Address might be overkill.</p>
<p>The middleware is configured like this in the service configuration during startup:</p>
<pre><code class="language-cs">builder.Services.Configure&lt;ForwardedHeadersOptions&gt;(options =&gt;
{
    options.ForwardLimit = 2;
    options.KnownProxies.Add(IPAddress.Parse(&quot;127.0.10.1&quot;));
    options.ForwardedForHeaderName = &quot;X-Forwarded-For-My-Custom-Header-Name&quot;;
});

...

// near the very top of the middleware pipeline
// to ensure subsequent middleware pieces get the updated addresses
app.UseForwardedHeaders();
</code></pre>
<p>There's more info on the Microsoft site on <a href="https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0">how the middleware works  here</a>.</p>
<p><a href="https://goldenbeartshirts.com" target="_blank">
<img src="https://weblog.west-wind.com/images/sponsors/BearingDownTheMountain-DisplayAd.jpg" class="da-content-image" />
</a></p>
<h3 id="summary">Summary</h3>
<p>Nothing new here, but given how often I fumble around with this value, creating a wrapper and putting a reminder here for quick lookup seems worth the effort 😄</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/RickStrahl/Westwind.AspNetCore">Westwind.AspNetCore on Github</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-10.0">Configure ASP.NET Core to work with proxy servers and load balancers</a></li>
</ul>
<div style="margin-top: 30px;font-size: 0.8em;
            border-top: 1px solid #eee;padding-top: 8px;">
    <img src="https://markdownmonster.west-wind.com/favicon.png" style="height: 20px;float: left; margin-right: 10px;">
    this post created and published with the 
    <a href="https://markdownmonster.west-wind.com" target="top">Markdown Monster Editor</a> 
</div>
]]></description>
      <link>https://weblog.west-wind.com/posts/2026/May/13/Getting-the-Client-IP-Address-in-ASPNET-Core</link>
      <guid isPermaLink="false">dkvnwxdbh42n</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/May/13/Getting-the-Client-IP-Address-in-ASPNET-Core#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/May/13/Getting-the-Client-IP-Address-in-ASPNET-Core</guid>
      <pubDate>Wed, 13 May 2026 10:35:26 GMT</pubDate>
      <abstract><![CDATA[When I need to pick up the client IP Address in ASP.NET Core I always forget where to find the connection information and/or forget about picking proxy forwarding instead of the actual IP address. To make things easy and reusable, here's a small HttpRequest extension method.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Getting-the-Client-IP-Address-in-ASP-NET-Core/ClientIpBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Putting the Westwind.Scripting C# Templating Library to work, Part 2</title>
      <description><![CDATA[<p><img src="https://weblog.west-wind.com/imageContent/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/Part2-Banner.jpg" alt="Part2 Banner"></p>
<blockquote>
<p>This is a two part series that discusses the Westwind.Scripting Template library</p>
<ul>
<li><p><a href="https://weblog.west-wind.com/posts/2026/Apr/20/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1">Part 1: Revisiting C# Scripting with the Westwind.Scripting Templating Library</a></p>
</li>
<li><p><strong>Part 2</strong>: Real world integration for a Local Rendering and Web Site Generation
<small> <em>(this post)</em></small></p>
</li>
</ul>
</blockquote>
<p>In part 1 of this series I introduced the <code>Westwind.Scripting</code> library, how it works and how you can use it and integrate it into your applications. In part 2, I'm going over implementation details of hosting a scripting engine for a specific scenario that generates local static Html output for a for a project based solution that requires both local preview and local Web site generation. As part of that process I'll point out a number of issues that you likely have to consider in this somewhat common scenario.</p>
<p>If you haven't already, I'd recommend you <a href="https://weblog.west-wind.com/posts/2026/Apr/20/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1">read Part 1</a> so you have a good idea what the library provides and how it works. While not required, this post will make a lot more sense with that context in place.</p>
<ul>
<li><a href="https://weblog.west-wind.com/posts/2026/Apr/20/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1">Revisiting Westwind.Script Template Scripting Library, Part 1</a></li>
</ul>
<p><a href="https://markdownmonster.west-wind.com?ut=weblog"  target="_blank"
	  title="Markdown Monster - Easy to use, yet powerfully productive Markdown Editing for Windows">
<img src="https://weblog.west-wind.com/images/sponsors/banner-example.png?v=1.2" class="da-content-image" />
</a></p>
<h2 id="putting-templating-to-use-in-a-real-world-scenario">Putting Templating to use in a Real World Scenario</h2>
<p>The <code>ScriptParser</code> class in the <code>Westwind.Scripting</code> library allows you to execute C# based, Handlebars-like templates that merge template text with model data that can be embedded into the template with Handlebars style <code>{{ expression }}</code> and <code>{{% code block }}</code> directives. You can use both string based templates and templates from files that can also include references to partials and layout pages. All of that was covered in Part 1.</p>
<p>While using the <code>ScriptParser</code> for demos and single template output is easy enough, using it to integrate into a larger application, interacting with host application features and content, and especially generating many document Web site output with related dependencies, requires a bit more work.</p>
<p>I'm using my <a href="https://documentationmonster.com">Documentation Monster</a> application as an example here as it's been my dog-fooding project to put the <code>ScriptParser</code> to real world use. It's a project based documentation solution that statically produces Web Site Html output. It generates Html output in two ways in an offline desktop application:</p>
<ul>
<li>Renders a single topic for Live Preview as you type topic content</li>
<li>Renders many topics in a documentation project into a full self-contained Web site</li>
</ul>
<p>DM uses script templates to render each topic with a specific topic type - Topic, Header, ClassHeader, ClassMethod, ClassProperty, WhatsNew, ExternalLink etc. -  each representing a separate Html template in an Html file (ie. <code>Topic.html</code>) on disk which are the templates I'm passing into the <code>ScriptParser</code> class for execution. The model then feeds specific a specific model that contains the topic, project and other support data and some application logic.</p>
<p>These topic templates templates are similar but all have overlapping content: All of them have a header and topic body, but they also have customized areas to them: For example, <em>ClassHeader</em> has a class member table, inheritance list, lists assembly and namespace, <strong>ClassProperty</strong>/<strong>ClassMethod</strong>/<strong>ClassField</strong> member templates have syntax and exception settings, <strong>ExternalLink</strong> displays an Html page during development but redirects to a Url in a published output file etc. In other words, each topic type has some unique things going on with it that the template script reflects. If a topic has a type that can't map to a template, the default template which in this case is the <strong>Topic</strong> template is used via fallback.</p>
<p>Templates are user customizable, so they are sensitive to changes and are recompiled whenever changes are <strong>detected</strong> in the generated code.</p>
<p>The raw Html rendering of topics is simple enough - the template is executed as is and produces Html output. But once you introduce document dependencies  like images, scripts, css etc. and you create output that may end up in nested folders, <strong>pathing becomes a concern</strong> in statically generated content. The reason is, the 'hosting' environment can't be determined at render time and the content may be hosted locally via file system (for individual preview in this case), a root Web site, or in a sub-folder of a Web site.</p>
<p>So then the question is what's a relative path based on? What's a root path (<code>/</code>) based on? The rendered output has to be self-contained, and templates are responsible for properly making paths natural to use for a specific output environment.</p>
<p>This means some or all Urls may have to be fixed up and in some cases a <code>&lt;base&gt;</code> has to be provided in the Html content for each page. None of that can happen automatically so this is a manual post-processing step that is application specific.</p>
<p>In addition, when rendering topics a few things that need to be considered:</p>
<ul>
<li>When and how to render using the ScriptParser</li>
<li>Where to store the Templates consistently</li>
<li>Ensure that rendered output can be referenced relatively</li>
<li>Ensure there's consistent BasePath to reference
root and project wide Urls (ie. <code>/</code> or <code>~/</code>)</li>
</ul>
<p>Let's walk through what template rendering looks like inside of an application.</p>
<p><a href="https://www.amazon.com/dp/BT00LN946S?externalReferenceId=fdeaac92-deca-4ea5-92c1-442cdc15a646"  target="_blank"
		  title="Sign up for an Amazon Visa - 5% cash back for Amazon and Whole Food Purchases">
<img src="https://weblog.west-wind.com/images/sponsors/AmazonPrimeVisa-Display.jpg" class="da-content-image" />
</a></p>
<h3 id="create-a-templatehost">Create a TemplateHost</h3>
<p>In projects that use template rendering I like to create a <strong>TemplateHost</strong> class that encapsulates all tasks related to executing the scripting engine. This simplifies configuration of the template engine in one place and provides a few easily accessible methods for rendering templates - in the case of DM rendering topics to string and to file.</p>
<h4 id="scriptparser-configuration---references-and-namespaces">ScriptParser Configuration - References and Namespaces</h4>
<p>Adding of dependent references and namespaces is one of the most frustrating things of doing runtime compilation of code and so that step needs to be consolidated into a single place.</p>
<p>Here's the base implementation of the TemplateHost with the <code>CreateScriptParser()</code> method implementation from DM:</p>
<pre><code class="language-csharp">public class TemplateHost
{
    public ScriptParser Script
    {
        get
        {
            if (field == null)
                field = CreateScriptParser();
            return field;
        }
        set;
    }

    public static ScriptParser CreateScriptParser()
    {
        var script = new ScriptParser();            
        
        // Good chunk of .NET Default libs
        script.ScriptEngine.AddDefaultReferencesAndNamespaces();
        
        // Any library in Host app that's been loaded up to this point
        // In DM everything required actually is loaded through this
        script.ScriptEngine.AddLoadedReferences();
        
        // explicit assemblies not or not yet used by host
        //script.ScriptEngine.AddAssembly(&quot;privatebin/Westwind.Ai.dll&quot;);
        //script.ScriptEngine.AddAssembly(typeof(Westwind.Utilities.StringUtils));

        script.ScriptEngine.AddNamespace(&quot;Westwind.Utilities&quot;);
        script.ScriptEngine.AddNamespace(&quot;DocMonster&quot;);
        script.ScriptEngine.AddNamespace(&quot;DocMonster.Model&quot;);            
        script.ScriptEngine.AddNamespace(&quot;DocMonster.Templates&quot;);
        script.ScriptEngine.AddNamespace(&quot;MarkdownMonster&quot;);
        script.ScriptEngine.AddNamespace(&quot;MarkdownMonster.Utilities&quot;);

        script.ScriptEngine.SaveGeneratedCode = true;
        script.ScriptEngine.CompileWithDebug = true;            

        // {{ expr }} is Html encoded - {{! expr }} required for raw Html output
        script.ScriptingDelimiters.HtmlEncodeExpressionsByDefault = true;            

		// custom props that expose in the template without Model. prefix
        script.AdditionalMethodHeaderCode =
            &quot;&quot;&quot;
            DocTopic Topic = Model.Topic;
            var Project = Model.Project;
            var Configuration = Model.Configuration;
            var Helpers = Model.Helpers;                
            var DocMonsterModel = Model.DocMonsterModel;
            var AppModel = MarkdownMonster.mmApp.Model;
            
            var BasePath = new Uri(FileUtils.NormalizePath( Project.ProjectDirectory + &quot;\\&quot;) );

            &quot;&quot;&quot;;            
        return script;
    }
    
    // render methods below
}
</code></pre>
<p>In DM the TemplateHost is created on first access of a <code>Project.TemplateHost</code> property and then persists for the lifetime of the project, unless explicitly recreated. There is some overhead in creating the script parser environment and we might be generating <strong>a lot</strong> of documents very quickly when generating Web site output so a cached instance is preferred.</p>
<pre><code class="language-csharp">get
{
    if (field == null)
        field = CreateScriptParser();
    return field;
}
</code></pre>
<p>Notice that the parser is set up with common default and all loaded assembly references from the host. I then add all the specific libraries that may not have been loaded yet, and any custom namespaces that are used by the various application specific components that are used in the templates. This is perhaps the main reason to use a <code>TemplateHost</code> like wrapper: To hide away all this application specific configuration for a one time config and then can be forgotten about - you don't want to be doing this sort of thing in your application or business logic code.</p>
<p>In DM I'm lucky enough that all application dependencies live in a couple of assemblies that are already loaded by the time the parser is activated so I can rely on <code>script.ScriptEngine.AddLoadedReferences()</code> to bring in all of my dependencies. If your application is broken out into many small dependencies that may not work as some assemblies may not have loaded yet in which case you have to ensure you manually use <code>script.AddAssemblyReference()</code> to pull in explicit assemblies preferrably using the <code>Type</code> overload.</p>
<p>Another thing of note in this particual usage scenario: The <code>AdditionalMethodHeaderCode</code> property is used to expose various objects as top level objects to the template script. So rather than having to specify <code>{{ Model.Topic.Title }}</code> we can just use <code>{{ Topic.Title }}</code> and <code>{{ Helpers.ChildTopicsList() }}</code> for example. Shortcuts are useful, and you can stuff anything you want to expose in the script beyond the model here.</p>
<blockquote>
<p>Since the templates in DM are accessible to end-users for editing, making the template expressions simpler makes for a more user friendly experience. Highly recommended.</p>
</blockquote>
<h4 id="rendering">Rendering</h4>
<p>The actual render code is pretty straight forward by calling <code>RenderTemplateFile()</code> which renders a template from file:</p>
<pre><code class="language-cs">public string RenderTemplateFile(string templateFile, RenderTemplateModel model)
{
    ErrorMessage = null;

    Script.ScriptEngine.ObjectInstance = null; // make sure we don't cache
    
    // explicitly turn these off for live output
	Script.ScriptEngine.SaveGeneratedCode = false;
	Script.ScriptEngine.CompileWithDebug = false;
	Script.ScriptEngine.DisableAssemblyCaching = false;
    
    string basePath = model.Project.ProjectDirectory;
    model.PageBasePath = System.IO.Path.GetDirectoryName(model.Topic.RenderTopicFilename);

    string result = Script.ExecuteScriptFile(templateFile, model, basePath: basePath);

    if (Script.Error)
	{
	    // run again this time with debugging options on
	    Script.ScriptEngine.SaveGeneratedCode = true;
	    Script.ScriptEngine.CompileWithDebug = true;
	    Script.ScriptEngine.DisableAssemblyCaching = true;  // force a recompile
	
	    Script.ExecuteScriptFile(templateFile, model, basePath: basePath);
	
	    Script.ScriptEngine.SaveGeneratedCode = false;
	    Script.ScriptEngine.CompileWithDebug = false;
	    Script.ScriptEngine.DisableAssemblyCaching = false;
	
	    // render the error page
	    result = ErrorHtml(model);
	    ErrorMessage = Script.ErrorMessage + &quot;\n\n&quot; + Script.GeneratedClassCodeWithLineNumbers;
	}

    return result;
}
</code></pre>
<p>This is the basic raw template execution logic that produces direct generated output - in this case Html.</p>
<p>Note that the processing checks for template errors which captures either compilation or runtime errors. If an error occurs, the current render process is re-run with all the debug options turned on so I can get additional error information to display on the error page.</p>
<p>I'll talk more about the error display in a minute.</p>
<h4 id="template-layout">Template Layout</h4>
<p>I haven't talked about what the templates look like: DM uses relatively small topic templates, with a more complex Layout page that provides for the Web site's page chrome. The actual project output renders both the content the headers and footers and there's a bunch of logic to pull in the table of contents and handle navigation to new topics efficiently. The preview renders the same content but some of the aspects like the table of content are visually hidden in that mode.</p>
<p>All of that logic is encapsulated in the layout page and the supporting JavaScript scripts.</p>
<p>At the core re the Html/Handlebars topic templates. As mentioned, each topic type is a template that is rendered. Topic, Header, ExternalLink, WhatsNew, ClassHeader, ClassProperty, ClassMethod etc. each with their own custom formats. Each of the templates then references the same layout page (you could have several different one however if you chose)</p>
<p>Here's an example <strong>Content Page</strong>:</p>
<p><strong>topic.html Template</strong></p>
<pre><code class="language-html">{{%
    Script.Layout = &quot;_layout.html&quot;;
}}

&lt;h2 class=&quot;content-title&quot;&gt;
    &lt;img src=&quot;/_docmonster/icons/{{ Model.Topic.DisplayType }}.png&quot;&gt;
    {{ Model.Topic.Title }}
&lt;/h2&gt;

&lt;div class=&quot;content-body&quot; id=&quot;body&quot;&gt;
    {{% if (Topic.IsLink &amp;&amp; Topic.Body.Trim().StartsWith(&quot;http&quot;)) { }}
        &lt;ul&gt;
        &lt;li&gt;
            &lt;a href=&quot;{{! Model.Topic.Body }}&quot; target=&quot;_blank&quot;&gt;{{ Model.Topic.Title }}&lt;/a&gt;
            &lt;a href=&quot;{{! Model.Topic.Body }}&quot; target=&quot;_blank&quot;&gt;&lt;i class=&quot;fa-solid fa-up-right-from-square&quot; style=&quot;font-size: 0.7em; vertical-align: super;&quot;&gt;&lt;/i&gt;&lt;/a&gt;
        &lt;/li&gt;
        &lt;/ul&gt;

        &lt;blockquote style=&quot;font-size: 0.8em;&quot;&gt;&lt;i&gt;In rendered output this link opens in a new browser window.
            For preview purposes, the link is displayed in this generic page.
            You can click the link to open the browser with the link which is the behavior you see when rendered.&lt;/i&gt;
        &lt;/blockquote&gt;
    {{% } else { }}
        {{ Model.Helpers.Markdown(Model.Topic.Body) }}
    {{% } }}
    
&lt;/div&gt;

{{% if (!string.IsNullOrEmpty(Model.Topic.Remarks)) {  }}
    &lt;h3 class=&quot;outdent&quot; id=&quot;remarks&quot;&gt;Remarks&lt;/h3&gt;
    {{ Helpers.Markdown(ModelTopic.Remarks) }}
{{% } }}


{{% if (!string.IsNullOrEmpty(Topic.Example))  {  }}
    &lt;h3 class=&quot;outdent&quot; id=&quot;example&quot;&gt;Example&lt;/h3&gt;
    {{ Helpers.Markdown(Topic.Example) }}
{{% } }}

{{% if (!string.IsNullOrEmpty(Topic.SeeAlso)) { }}
    &lt;h4 class=&quot;outdent&quot; id=&quot;seealso&quot;&gt;See also&lt;/h4&gt;
    &lt;div class=&quot;see-also-container&quot;&gt;
        {{ Helpers.FixupSeeAlsoLinks(Topic.SeeAlso) }}
    &lt;/div&gt;
{{% } }}
</code></pre>
<p>For demonstration purposes I'm showing both the <code>Model.Topic</code> and the custom header based direct binding to <code>Topic</code> via <code>script.AdditionalMethodHeaderCode</code> I showed earlier. Both point at the same value.</p>
<p>Note also the code block at the top that pulls in the Layout page:</p>
<pre><code class="language-html">{{%
    Script.Layout = &quot;_layout.html&quot;;
}}
</code></pre>
<p>The layout page then looks like this:</p>
<p><strong>_Layout.html</strong></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    {{%
     var theme = Project.Settings.RenderTheme;
     if(Topic.TopicState.IsPreview) { }}
    &lt;base href=&quot;{{ Model.PageBasePath }}&quot; /&gt;
    {{% } }}

    &lt;meta charset=&quot;utf-8&quot; /&gt;
    &lt;title&gt;{{ Topic.Title }} - {{ Project.Title }}&lt;/title&gt;

    {{% if (!string.IsNullOrEmpty(Topic.Keywords)) { }}
    &lt;meta name=&quot;keywords&quot; content=&quot;{{ Topic.Keywords.Replace(&quot; \n&quot;,&quot;, &quot;) }}&quot; /&gt;
    {{% } }}
    {{% if(!string.IsNullOrEmpty(Topic.Abstract)) { }}
    &lt;meta name=&quot;description&quot; content=&quot;{{! Topic.Abstract }}&quot; /&gt;
    {{% } }}
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1,maximum-scale=1&quot; /&gt;
    &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;~/_docmonster/themes/scripts/bootstrap/bootstrap.min.css&quot; /&gt;
    &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;~/_docmonster/themes/scripts/fontawesome/css/font-awesome.min.css&quot; /&gt;
    &lt;link id=&quot;AppCss&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;~/_docmonster/themes/{{ theme }}/docmonster.css&quot; /&gt;

    &lt;script src=&quot;~/_docmonster/themes/scripts/highlightjs/highlight.pack.js&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;~/_docmonster/themes/scripts/highlightjs-badge.min.js&quot;&gt;&lt;/script&gt;
    &lt;link href=&quot;~/_docmonster/themes/scripts/highlightjs/styles/vs2015.css&quot; rel=&quot;stylesheet&quot; /&gt;
    &lt;script src=&quot;~/_docmonster/themes/scripts/bootstrap/bootstrap.bundle.min.js&quot; async&gt;&lt;/script&gt;
    &lt;script src=&quot;~/_docmonster/themes/scripts/lunr/lunr.min.js&quot;&gt;&lt;/script&gt;
    &lt;script&gt;
        window.page = {};
        window.page.basePath = &quot;{{ Project.Settings.RelativeBaseUrl }}&quot;;
        window.renderTheme=&quot;{{ Project.Settings.RenderThemeMode }}&quot;;
    &lt;/script&gt;
    &lt;script src=&quot;~/_docmonster/themes/scripts/docmonster.js&quot;&gt;&lt;/script&gt;

    {{% if(Topic.TopicState.IsPreview) { }}
    &lt;!-- Preview Navigation and Syncing --&gt;
    &lt;script src=&quot;~/_docmonster/themes/scripts/preview.js&quot;&gt;&lt;/script&gt;
    {{% } }}

&lt;/head&gt;
&lt;body&gt;
    &lt;!-- Markdown Monster Content --&gt;
    &lt;div class=&quot;flex-master&quot;&gt;
        &lt;div class=&quot;banner&quot;&gt;
            &lt;div class=&quot;float-end&quot;&gt;
                &lt;button id=&quot;themeToggleBtn&quot; type=&quot;button&quot; onclick=&quot;toggleTheme()&quot;
                        class=&quot;btn btn-sm btn-secondary theme-toggle&quot;
                        title=&quot;Toggle Light/Dark Theme&quot;&gt;
                    &lt;i id=&quot;themeToggleIcon&quot;
                       class=&quot;fa fa-moon text-warning&quot;&gt;
                    &lt;/i&gt;
                &lt;/button&gt;
            &lt;/div&gt;

            &lt;div class=&quot;float-start sidebar-toggle&quot;&gt;
                &lt;i class=&quot;fa fa-bars&quot;
                   title=&quot;Show or hide the topics list&quot;&gt;&lt;/i&gt;
            &lt;/div&gt;

			{{% if (Topic.Incomplete) { }}
               &lt;div class=&quot;float-end mt-2 &quot; title=&quot;This topic is under construction.&quot;&gt;
                   &lt;i class=&quot;fa-duotone fa-triangle-person-digging fa-lg fa-beat&quot;
                   style=&quot;--fa-primary-color: #333; --fa-secondary-color: goldenrod; --fa-secondary-opacity: 1; --fa-animation-duration: 3s;&quot;&gt;&lt;/i&gt;
	           &lt;/div&gt;
		    {{% } }}

            &lt;img src=&quot;~/images/logo.png&quot; class=&quot;banner-logo&quot; /&gt;
            &lt;div class=&quot;projectname&quot;&gt; {{ Project.Title }}&lt;/div&gt;

            &lt;div class=&quot;byline&quot;&gt;
                &lt;img src=&quot;~/_docmonster/icons/{{ Topic.DisplayType }}.png&quot;&gt;
                {{ Topic.Title }}
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=&quot;page-content&quot;&gt;
            &lt;div id=&quot;toc-container&quot; class=&quot;sidebar-left toc-content&quot;&gt;
                &lt;nav class=&quot;visually-hidden&quot;&gt;
                    &lt;a href=&quot;~/tableofcontents.html&quot;&gt;Table of Contents&lt;/a&gt;
                &lt;/nav&gt;
            &lt;/div&gt;

            &lt;div class=&quot;splitter&quot;&gt;
            &lt;/div&gt;

            &lt;nav class=&quot;topic-outline&quot;&gt;
                &lt;div class=&quot;topic-outline-header&quot;&gt;On this page:&lt;/div&gt;
                &lt;div class=&quot;topic-outline-content&quot;&gt;&lt;/div&gt;
            &lt;/nav&gt;

            &lt;div id=&quot;MainContent&quot; class=&quot;main-content&quot;&gt;
                &lt;!-- Rendered Content --&gt;

                &lt;article class=&quot;content-pane&quot;&gt;
                    {{ Script.RenderContent() }}
                &lt;/article&gt;

                &lt;div class=&quot;footer&quot;&gt;

                    &lt;div class=&quot;float-start&quot;&gt;
                        &amp;copy; {{ Project.Owner }}, {{ DateTime.Now.Year }} &amp;bull;
                        updated: {{ Topic.Updated.ToString(&quot;MMM dd, yyyy&quot;) }}
                        &lt;br /&gt;
                        {{%
                            string mailBody = $&quot;Project: {Project.Title}\nTopic: {Topic.Title}\n\nUrl:\n{ Project.Settings.WebSiteBaseUrl?.TrimEnd('/') + Project.Settings.RelativeBaseUrl }{ Topic.Id }.html&quot;;
                            mailBody = WebUtility.UrlEncode(mailBody).Replace(&quot;+&quot;, &quot;%20&quot;);
                        }}
                        &lt;a href=&quot;mailto:{{ Project.Settings.SupportEmail }}?subject=Support: {{ Project.Title }} - {{ Topic.Title }}&amp;body={{ mailBody }}&quot;&gt;Comment or report problem with topic&lt;/a&gt;
                    &lt;/div&gt;

                    &lt;div class=&quot;float-end&quot;&gt;
                        &lt;a href=&quot;https://documentationmonster.com&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;~/_docmonster/images/docmonster.png&quot; style=&quot;height: 3.8em&quot;/&gt;&lt;/a&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
                &lt;!-- End Rendered Content --&gt;
            &lt;/div&gt; &lt;!-- End MainContent --&gt;
        &lt;/div&gt; &lt;!-- End page-content --&gt;
    &lt;/div&gt;   &lt;!-- End flex-master --&gt;
    
    &lt;!-- End Markdown Monster Content --&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The full rendered site output looks something like this for a topic:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/RenderedHtmlSite.png" alt="Documentation Monster Rendered Html Site"><br>
<small><strong>Figure 1</strong> - Documentation Monster Rendered site output</small></p>
<p>The rendered topic content is in the middle panel of the display - all the rest is both static and dynamically rendered Html that is controlled through the Layout page. Both the table of contents and the document outline on the right are rendered using dynamic loading via JavaScript, while the header and footer are static with some minor embedded expressions.</p>
<p>With a Layout page this is easy to set up and maintain as there's a single page that handles that logic and it's easily referenced from each of the topic templates with a single layout page reference.</p>
<p>Behind the scenes, the parser looks for a Layout page directive in the content page requested by the <code>ScriptParser</code>, and if it finds one combines the layout page and content page into a single page that is executed. Essentially the <code>Script.Layout</code> command in the content page causes the referenced Layout page to be loaded, and the <code>{{ Script.RenderContent() }}</code> tag then is replaced with the content page content resulting in a single Html/Handlebars template. This combined content is then compiled and executed to produce the final merged output.</p>
<p><a href="https://websurge.west-wind.com?ut=weblog"  target="_blank"
	  title="West Wind WebSurge - A powerful, yet easy to use REST Client and HTTP Load Testing tool for Windows">
<img src="https://weblog.west-wind.com/images/Sponsors/Websurge-Display.png" class="da-content-image" />
</a></p>
<p>So far things are pretty straight forward relying on the core features of the scripting engine: We're pointing at template and a layout page and it produces Html in various forms depending on the type of template that we are dealing with.</p>
<p>That still leaves the task fixing up paths to make sure they work in the final 'hosting' environment.</p>
<h3 id="base-path-handling---pagebasefolder-and-basefolder">Base Path Handling - PageBaseFolder and BaseFolder</h3>
<p>When rendering Html output that depends on other resources like images CSS and scripts that referenced either as relative or site rooted paths, it's important that the page context can find these resources based on natural relative and absolute page path resolution.</p>
<p>There are two concerns:</p>
<ul>
<li>Page Base Path for page relative links</li>
<li>Project Base Path for site root paths</li>
</ul>
<h4 id="page-base-path-for-relative-linking">Page Base Path for Relative Linking</h4>
<p>Page base path refers to resolving relative paths in the document. For example from the current page referencing an image as <code>SomeImage.png</code> (same folder) or <code>../images/SomeFolder</code> (relative path). In order for these relative paths to work the page has to be either running directly from the folder or the page has to be mapped into that page context.</p>
<p>In the context of a Web site that's simple as you have a natural page path that always applies. However, for previewing pages that are often rendered to a temporary file in an external location, which is then displayed in a WebView for preview. In that scenario relative path needs to be fixed up so it can find resolve links.</p>
<p>This scenario can be handled by explicitly forcing the page's <code>&lt;base&gt;</code> path to reflect the current page's path:</p>
<pre><code class="language-html">{{%
    if(Topic.TopicState.IsPreview)  { }}
       &lt;base href=&quot;{{ Model.PageBasePath }}&quot; /&gt;
{{%  }  }}
</code></pre>
<p>Note that I'm only applying the <code>&lt;base&gt;</code> tag in Preview mode. In Preview the Html is rendered into a temp location, so I map back to the actual location where relative content is expected and that makes relative links work.</p>
<p>Here's what that looks like:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/PageBasePathAndRootPath.png" alt="Page Base Path And Root Path"><br>
<small><strong>Figure 2</strong> - Providing a Page Base path when rendering to a temp location is crucial to ensure relative resources like images can be found!</small></p>
<p>Without the <code>&lt;base&gt;</code> path in the document, the page would look for the image in the rendered output location - in the <code>TMP</code> folder - and of course would not find the image there.</p>
<p>For final rendered output running in a Web Browser, this is not necessary as the page naturally runs out of the appropriate folder and no <code>&lt;base&gt;</code> path is applied.</p>
<blockquote>
<p><em>Why not just render local output into the 'correct relative location' where relative content can be found?</em><br>
For local preview that's often impractical due to permissions or simply for cluttering up folders with temporary render files. Generated files can wreak havoc with source control or Dropbox and permissions unless explicit exceptions are set up. I recommend that local WebView content that has dependencies should always be rendered to a temporary location and then be back-linked via <code>&lt;base&gt;</code> paths to ensure that relative paths can resolve.</p>
</blockquote>
<h4 id="root-paths-in-temporary-location">Root Paths In Temporary Location</h4>
<p>The other more important issue has to do with resolving the root path. This is especially important when rendering to a temporary file, but it can even be an issue if you create a 'site' that sits off a Web root.</p>
<p>For example, most of my documentation 'sites' live in a <code>/docs</code> folder off the main Web site rather than at the <code>/</code> root.</p>
<p>For example:</p>
<ul>
<li><a href="https://markdownmonster.west-wind.com/docs/">Markdown Monster Documentation</a></li>
</ul>
<p>In this site, any links that reference <code>/</code> and intend to go back to the documentation root, are going to jump back to the main Web site root instead. From a project perspective I want <code>/</code> to mean the project root, but I don't want to have to figure out while I'm working on the content whether I have to use <code>/docs/</code> or <code>/</code>. In fact, I never want to hard code a reference to <code>/docs/</code> in my templates or in user content, but expect to reference the project root as <code>/</code>.</p>
<p>In DM this is very relevant when the site is generated. We can then specify a root folder <code>/</code> by default or <code>/docs/</code> explicitly as shown here entered for my specific site:</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/ProvidingPageBasePathWhenRendering.png" alt="Providing Page Base Path When Rendering"><br>
<small><strong>Figure 3</strong> - When rendering to a non-root location it has to be generated at Html generation time.</small></p>
<p>Here's what this looks like in the template <em>(you can use either <code>~/</code> or <code>/</code>)</em>:</p>
<pre><code class="language-html">&lt;link id=&quot;AppCss&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot; 
      href=&quot;~/_docmonster/themes/{{ theme }}/docmonster.css&quot; /&gt;
</code></pre>
<p>and here is the rendered output with the <code>/docs/</code> path for any <code>/</code> or <code>~/</code> starting Urls:</p>
<pre><code class="language-html">&lt;link id=&quot;AppCss&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot; 
      href=&quot;/docs/_docmonster/themes/Dharkan/docmonster.css&quot; /&gt;
</code></pre>
<p>This fixup applies to any Url generated both from the templates and from user generated topic content so the fixup has to occur post rendering - it's not something that can be fixed via the template.</p>
<p>This fixup also happens in Preview mode where the full path is prefixed which produces these nasty looking paths:</p>
<pre><code class="language-html">&lt;link id=&quot;AppCss&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot; 
      href=&quot;C:/Users/rstrahl/Documents/Documentation Monster/MarkdownMonster/_docmonster/themes/Dharkan/docmonster.css&quot; /&gt;
</code></pre>
<p>This means that the root path need to be fixed up depending on the root path environment. There are several scenarios:</p>
<ul>
<li>Root is root <code>/</code> rendered into root of site for final Html Site output - no replacements required.</li>
<li>Root is a subfolder (ie. <code>/docs/</code>) - <code>/</code> is replaced with <code>/docs/</code> in all paths</li>
<li>Root is the project folder from Temp location - <code>/</code> is replaced with a folder file location or WebView virtual host name root</li>
</ul>
<p>This is something that is not part of the <code>ScriptParser</code> class, but rather has to be handled at the application layer post fix up, and it's fairly specific to the Html based generation that takes place. In my template based applications I <strong>always</strong> have do one form or another of this sort of fix up.</p>
<p>Here's what this looks like in DM:</p>
<pre><code class="language-csharp">string html = Project.TemplateHost.RenderTemplateFile(templateFile, model);

...

// Fix up any locally linked .md extensions to .html
string basePath = null;
if (renderMode == TopicRenderModes.Html)
{
 	 // fix up DM specific `dm-XXX` links like topic refs, .md file links etc.
     html = FixupHtmlLinks(html, renderMode);  

     // Specified value in dialog ('/docs/` or `/`)
     basePath = Project.Settings.RelativeBaseUrl;  // 
}            
else if (renderMode == TopicRenderModes.Preview || renderMode == TopicRenderModes.Chm)
{
	// special `dm-XXX` links are handled via click handlers
	
	// Project directory is our base folder
    basePath = Path.TrimEndingDirectorySeparator(Project.ProjectDirectory).Replace(&quot;\\&quot;, &quot;/&quot;) + &quot;/&quot;;
}

html = html
           .Replace(&quot;=\&quot;/&quot;, &quot;=\&quot;&quot; + basePath)
           .Replace(&quot;=\&quot;~/&quot;, &quot;=\&quot;&quot; + basePath)
           // UrlEncoded
           .Replace(&quot;=\&quot;%7E/&quot;, &quot;=\&quot;&quot; + basePath)
           // Escaped
          .Replace(&quot;=\&quot;\\/&quot;, &quot;=\&quot;/&quot;)
          .Replace(&quot;=\&quot;~\\/&quot;, &quot;=\&quot;/&quot;);
          
if(renderMode == TopicRenderModes.Preview ||
   renderMode == TopicRenderModes.Print || 
   renderMode == TopicRenderModes.Chm)
{
   // explicitly specify local page path
   var path = Path.GetDirectoryName(topic.GetExternalFilename()).Replace(&quot;\\&quot;,&quot;/&quot;) + &quot;/&quot;;
   html = html.Replace(&quot;=\&quot;./&quot;, &quot;=\&quot;&quot; + path)
              .Replace(&quot;=\&quot;../&quot;, &quot;=\&quot;&quot; + path + &quot;../&quot;);
}

OnAfterRender(html, renderMode);

return html;          
</code></pre>
<p>The key piece is the Html fix up block that takes any <code>/</code> or <code>~/</code> links - including some variations - and explicitly replaces the actual base path that's specified.</p>
<p>Another issue is addressed in the following block that deals with print and local pages: Relative links don't resolve in print and PDF output, so they have to be explicitly specfied as part of the link. Apparently the WebView print engine ignores <code>&lt;basePath&gt;</code> for many things and so even relative links have to be fixed up with an explicit prefix even if the page is in the correct location.</p>
<p>So yeah - path handling is a pain in the ass! 🤣 But hopefully this section gives you the information you need to fix up your paths as needed.</p>
<p><a href="https://www.linqpad.net/?affiliate=4n4zaa6t"  target="_blank"
		  title="LinqPad - The Scratchpad for .NET Developers">
<img src="https://weblog.west-wind.com/images/sponsors/LinqPad-DisplayAd.jpg" class="da-content-image" />
</a></p>
<h3 id="error-handling">Error Handling</h3>
<p>When a template is run there is a possibility that it can fail. Typically it fails because there's an error in the template itself, which tends to generate compilation errors, or you can also run into runtime errors when code execution fails.</p>
<p>The <code>ScriptParser</code> automatically captures error information for both compilation and runtime errors are provides convenient members to access them. There's <code>ErrorMessage</code> and ``
If an error occurs, DM checks for it and then generates an error page:</p>
<pre><code>if (Script.Error)
{
    result = ErrorHtml();
    ErrorMessage = Script.ErrorMessage + &quot;\n\n&quot; + Script.GeneratedClassCodeWithLineNumbers;
}
</code></pre>
<p>where <code>ErrorHtml()</code> is the method that creates the error.</p>
<p>You can keep this real simple and just render the error message and optionally the code:</p>
<pre><code class="language-cs">public string ErrorHtml(string errorMessage = null, string code = null)
{            
    if (string.IsNullOrEmpty(errorMessage))
        errorMessage = Script.ErrorMessage;
    if (string.IsNullOrEmpty(code))
        code = Script.GeneratedClassCodeWithLineNumbers;

    string result =
            &quot;&lt;style&gt;&quot; +
            &quot;body { background: white; color; black; font-family: sans;}&quot; +
            &quot;&lt;/style&gt;&quot; +
            &quot;&lt;h1&gt;Template Rendering Error&lt;/h3&gt;\r\n&lt;hr/&gt;\r\n&quot; +
            &quot;&lt;pre style='font-weight: 600;margin-bottom: 2em;'&gt;&quot; + WebUtility.HtmlEncode(errorMessage) + &quot;&lt;/pre&gt;\n\n&quot; +
            &quot;&lt;pre&gt;&quot; + WebUtility.HtmlEncode(code) + &quot;&lt;/pre&gt;&quot;;                   

    return result;
}
</code></pre>
<p>Or you can present a nicer error page that itself is rendered through a template. This is the error page that is used in DM.</p>
<p><img src="https://weblog.west-wind.com/imageContent/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/DisplayErrorMessages.png" alt="Display Error Messages"><br>
<small><strong>Figure 4</strong> - Displaying render error information for debugging purposes</small></p>
<p>The code for this implementation is more complex.</p>
<pre><code class="language-csharp">public string ErrorHtml(RenderTemplateModel model, string errorMessage = null, string code = null)
  {
      if (string.IsNullOrEmpty(errorMessage))
          errorMessage = Script.ErrorMessage;
      if (string.IsNullOrEmpty(code))
          code = Script.GeneratedClassCodeWithLineNumbers;

      var origTemplateFile = model.Topic.GetRenderTemplatePath(model.Project.Settings.RenderTheme);            
      var templateFile = Path.Combine(Path.GetDirectoryName(origTemplateFile), &quot;ErrorPage.html&quot;);
      string errorOutput = null;

      if (File.Exists(templateFile))
      {
      	  // render the error template
          var lastException = Script.ScriptEngine.LastException;
          model?.TemplateError = new TemplateError
          {
              Message = errorMessage,
              GeneratedCode = code,
              TemplateFile = origTemplateFile,
              CodeErrorMessage = errorMessage,
              CodeLineError = string.Empty,
              Exception = lastException
          };
          model.TemplateError.Message = model.TemplateError.WrapCompilerErrors(model.TemplateError.Message);
          model.TemplateError.ParseCompilerError();

          // Try to execute ErrorPage.html template

          bool generateScript = Script.SaveGeneratedClassCode;
          Script.SaveGeneratedClassCode = false;

          errorOutput = Script.ExecuteScriptFile(templateFile, model, basePath: model.Project.ProjectDirectory);

          Script.SaveGeneratedClassCode = generateScript;
      }

	  // if template doesn't exist or FAILs render a basic error page
      if (string.IsNullOrEmpty(errorOutput))
      {
          if (Script.ErrorMessage.Contains(&quot; CS&quot;) &amp;&amp; Script.ErrorMessage.Contains(&quot;):&quot;))
          {
              errorOutput =
                      &quot;&lt;style&gt;&quot; +
                      &quot;body { background: white; color: black; font-family: sans-serif; }&quot; +
                      &quot;&lt;/style&gt;&quot; +
                      &quot;&lt;h1&gt;Template Compilation Error&lt;/h3&gt;\r\n&lt;hr/&gt;\r\n&quot; +
                      &quot;&lt;p style='margin-bottom: 2em; '&gt;&quot; + HtmlUtils.DisplayMemo(model.TemplateError.WrapCompilerErrors(errorMessage)) + &quot;&lt;/pre&gt;\n\n&quot; +
                      (model.Topic.TopicState.TopicRenderMode == TopicRenderModes.Preview
                          ? &quot;&lt;pre&gt;&quot; + WebUtility.HtmlEncode(code) + &quot;&lt;/pre&gt;&quot;
                          : &quot;&quot;);
          }
          else
          {
              errorOutput =
                  &quot;&lt;style&gt;&quot; +
                  &quot;body { background: white; color: black; font-family: sans-serif;}&quot; +
                  &quot;&lt;/style&gt;&quot; +
                  &quot;&lt;h1&gt;Template Rendering Error&lt;/h3&gt;\r\n&lt;hr/&gt;\r\n&quot; +
                   &quot;&lt;p style='font-weight: 600;margin-bottom: 2em; '&gt;&quot; + WebUtility.HtmlEncode(errorMessage) + &quot;&lt;/p&gt;\n\n&quot; +
                   (model.Topic.TopicState.TopicRenderMode == TopicRenderModes.Preview
                       ? &quot;&lt;hr/&gt;&lt;pre&gt;&quot; + WebUtility.HtmlEncode(code) + &quot;&lt;/pre&gt;&quot;
                       : &quot;&quot;);
          }
      }

      return errorOutput;
  }
</code></pre>
<h2 id="summary">Summary</h2>
<p>And that brings us to the end of this two-part post series. In this second part I've given a look behind the scenes of what's involved in running a scripting engine for site generation and local preview. As you can tell this is a little more involved than the basics of running a script template using the <code>ScriptParser</code> class as described in part one of this series.</p>
<p>Pathing is the main thing that can give you headaches - especially if there are multi-use cases of where your output is rendered. In the case of Documentation Monster, output can both be rendered into a final output Web site, or be used locally for previewing content from the file system and also be used for generating PDF and Print output which have additional specific requirements for pathing. The issues involved depend to some degrees on how you set up your application, but for me at least, I spent a lot of time getting the pathing to work correctly across all the output avenues. I hope that what I covered here will be of help in figuring out what needs to be considered, if not solving the issues directly.</p>
<p>I've been very happy with how using ScriptParser with its new features of Layout/Sections support works for this project. Given that I've been sitting in analysis paralysis for so long prior fighting with Razor, the results I've gotten are a huge relief, both in terms of features and functionality and performance. Hopefully the Westwind.Scripting library and the Script Templating will be useful to some of you as well.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://weblog.west-wind.com/posts/2026/Apr/20/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1">Part 1: Revisiting C# Scripting with the Westwind.Scripting Templating Library</a></li>
<li><a href="https://github.com/RickStrahl/Westwind.Scripting">Westwind.Scripting Library on GitHub</a></li>
<li><a href="https://github.com/RickStrahl/Westwind.Scripting/blob/master/ScriptAndTemplates.md">Westwind.Scripting Templating Features</a></li>
<li><a href="https://weblog.west-wind.com/posts/2022/Jun/07/Runtime-C-Code-Compilation-Revisited-for-Roslyn">Previous Post: Runtime Compilation with Roslyn and Building Westwind.Scripting</a></li>
<li><a href="https://documentationmonster.com">Documentation Monster</a></li>
<li><a href="https://markdownmonster.west-wind.com">Markdown Monster</a></li>
</ul>
]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Apr/23/Putting-the-WestwindScripting-Templating-Library-to-work-Part-2</link>
      <guid isPermaLink="false">5314605</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Apr/23/Putting-the-WestwindScripting-Templating-Library-to-work-Part-2#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Apr/23/Putting-the-WestwindScripting-Templating-Library-to-work-Part-2</guid>
      <pubDate>Thu, 23 Apr 2026 16:04:43 GMT</pubDate>
      <abstract><![CDATA[In part 2 of this post series I look at some of the issues you may have to deal with when using the Westwind.Scripting library as an offline document or Web site creation engine. While running simple templates is easy enough, when generating static output for Web site publishing or local preview requires some special considerations.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/Part2-Banner.jpg</featuredImage>
    </item>
    <item>
      <title>Revisiting C# Scripting with the Westwind.Scripting Templating Library, Part 1</title>
      <description><![CDATA[The `Westwind.Scripting` library provides runtime C# code compilation and execution as well as a C# based Script Template engine using Handlebars style syntax with pure C# code. In this post I discuss use cases for script templating and some examples of how I use in real-world applications, followed by a discussion of the engine's features and the new Layout, Section and Partials feature that was added recently.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Apr/01/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1</link>
      <guid isPermaLink="false">5311031</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Apr/01/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Apr/01/Revisiting-C-Scripting-with-the-WestwindScripting-Templating-Library-Part-1</guid>
      <pubDate>Wed, 01 Apr 2026 10:35:37 GMT</pubDate>
      <abstract><![CDATA[The `Westwind.Scripting` library provides runtime C# code compilation and execution as well as a C# based Script Template engine using Handlebars style syntax with pure C# code. In this post I discuss use cases for script templating and some examples of how I use in real-world applications, followed by a discussion of the engine's features and the new Layout, Section and Partials feature that was added recently.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Revisiting-C-Scripting-with-the-Westwind-Scripting-Templating-Library,-Part-1/Scripting-Banner.jpg</featuredImage>
    </item>
    <item>
      <title>Using .NET Native AOT to build Windows WinAPI Dlls</title>
      <description><![CDATA[Did you know that you can use .NET AOT compilation to create native Windows DLLs to potentially replace traditional C/C++ compiled DLLs? It's now possible to build completely native DLLs that can be called from external applications and legacy applications in particular using .NET AOT  compilation. In this post I show how this works and discuss the hits and misses of this tech.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Mar/21/Using-NET-Native-AOT-to-build-Windows-WinAPI-Dlls</link>
      <guid isPermaLink="false">5321191</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Mar/21/Using-NET-Native-AOT-to-build-Windows-WinAPI-Dlls#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Mar/21/Using-NET-Native-AOT-to-build-Windows-WinAPI-Dlls</guid>
      <pubDate>Sat, 21 Mar 2026 11:47:13 GMT</pubDate>
      <abstract><![CDATA[Did you know that you can use .NET AOT compilation to create native Windows DLLs to potentially replace traditional C/C++ compiled DLLs? It's now possible to build completely native DLLs that can be called from external applications and legacy applications in particular using .NET AOT  compilation. In this post I show how this works and discuss the hits and misses of this tech.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Using-NET-Native-AOT-to-build-Windows-WinAPI-Dlls/PostBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Azure Trusted Signing Revisited with Dotnet Sign</title>
      <description><![CDATA[In this follow-up post to my previous guide on Azure Trusted Signing, I  explore how the new `dotnet sign` tool significantly simplifies the code signing process compared to the traditional `SignTool` workflow. The post identifies `dotnet sign` using `artifact-signing` as a faster, more efficient alternative.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Mar/02/Azure-Trusted-Signing-Revisited-with-Dotnet-Sign</link>
      <guid isPermaLink="false">5249291</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Mar/02/Azure-Trusted-Signing-Revisited-with-Dotnet-Sign#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Mar/02/Azure-Trusted-Signing-Revisited-with-Dotnet-Sign</guid>
      <pubDate>Mon, 02 Mar 2026 10:10:23 GMT</pubDate>
      <abstract><![CDATA[In this follow-up post to my previous guide on Azure Trusted Signing, I  explore how the new `dotnet sign` tool significantly simplifies the code signing process compared to the traditional `SignTool` workflow. The post identifies `dotnet sign` using `artifact-signing` as a faster, more efficient alternative.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Azure-Trusted-Signing-Revisited-with-Dotnet-Sign/TrustedSigningBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Don't use the Microsoft Timestamp Server for Signing</title>
      <description><![CDATA[The default Microsoft timestamp server frequently causes intermittent failures during the code-signing process, particularly when processing many files or large binaries as part of a application distribution. These reliability issues can be resolved by replacing the Microsoft timestamp server with a more stable, compatible third-party alternative.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Feb/26/Dont-use-the-Microsoft-Timestamp-Server-for-Signing</link>
      <guid isPermaLink="false">5239628</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Feb/26/Dont-use-the-Microsoft-Timestamp-Server-for-Signing#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Feb/26/Dont-use-the-Microsoft-Timestamp-Server-for-Signing</guid>
      <pubDate>Thu, 26 Feb 2026 12:41:03 GMT</pubDate>
      <abstract><![CDATA[The default Microsoft timestamp server frequently causes intermittent failures during the code-signing process, particularly when processing many files or large binaries as part of a application distribution. These reliability issues can be resolved by replacing the Microsoft timestamp server with a more stable, compatible third-party alternative.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Dont-use-the-Microsoft-Timestamp-Server-for-Signing/SigningFailedBanner.png</featuredImage>
    </item>
    <item>
      <title>Reliably Refreshing the WebView2 Control</title>
      <description><![CDATA[The WebView2 control lacks a direct `Reload(noCache)` overload that forces a browser hard reload of the current page. Instead content is loaded with a soft refresh that - hopefully - reloads the current page and its dependencies dependent on WebView environment and server cache policies. In this post we'll look at how to work around this limitation and force a hard refresh in several different ways.]]></description>
      <link>https://weblog.west-wind.com/posts/2026/Feb/04/Reliably-Refreshing-the-WebView2-Control</link>
      <guid isPermaLink="false">5204283</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2026/Feb/04/Reliably-Refreshing-the-WebView2-Control#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2026/Feb/04/Reliably-Refreshing-the-WebView2-Control</guid>
      <pubDate>Wed, 04 Feb 2026 21:53:22 GMT</pubDate>
      <abstract><![CDATA[The WebView2 control lacks a direct `Reload(noCache)` overload that forces a browser hard reload of the current page. Instead content is loaded with a soft refresh that - hopefully - reloads the current page and its dependencies dependent on WebView environment and server cache policies. In this post we'll look at how to work around this limitation and force a hard refresh in several different ways.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2026/Reliably-Refreshing-the-WebView2-Control/WebViewRefreshBanner.jpg</featuredImage>
    </item>
    <item>
      <title>What the heck is a `\\.\nul` path and why is it breaking my Directory Files Lookup?</title>
      <description><![CDATA[I've been trying to track down an odd bug in my logs related to Null device failures in directory lookups which have been causing application level exceptions. In some odd scenarios these null mappings are showing up for files. In this post I take a look at what they are and how to work around them for this very edge case scenario.]]></description>
      <link>https://weblog.west-wind.com/posts/2025/Dec/08/What-the-heck-is-a-nul-path-and-why-is-it-breaking-my-Directory-Files-Lookup</link>
      <guid isPermaLink="false">5133488</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2025/Dec/08/What-the-heck-is-a-nul-path-and-why-is-it-breaking-my-Directory-Files-Lookup#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2025/Dec/08/What-the-heck-is-a-nul-path-and-why-is-it-breaking-my-Directory-Files-Lookup</guid>
      <pubDate>Mon, 08 Dec 2025 13:15:18 GMT</pubDate>
      <abstract><![CDATA[I've been trying to track down an odd bug in my logs related to Null device failures in directory lookups which have been causing application level exceptions. In some odd scenarios these null mappings are showing up for files. In this post I take a look at what they are and how to work around them for this very edge case scenario.]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2025/What-the-heck-is-a-nul-path-and-why-is-it-breaking-my-Directory-Files-Lookup/NullDeviceBanner.jpg</featuredImage>
    </item>
    <item>
      <title>Using the new WebView2 AllowHostInputProcessing Keyboard Mapping Feature</title>
      <description><![CDATA[If you've used the WebVIew2 control for a UI interactive Hybrid active you've probably run into some odd keyboard behavior, where some keys cannot be captured properly in the host application or are not forwarded quite the way they normally behave. In the newer releases of the WebView SDK there's a new option called `AllowHostInputProcessing` that allows for forwarding keyboard events more aggressively to the host which fixes some Windows behaviors and introduces some new issues. ]]></description>
      <link>https://weblog.west-wind.com/posts/2025/Aug/20/Using-the-new-WebView2-AllowHostInputProcessing-Keyboard-Mapping-Feature</link>
      <guid isPermaLink="false">4979824</guid>
      <author> (Rick Strahl)</author>
      <comments>https://weblog.west-wind.com/posts/2025/Aug/20/Using-the-new-WebView2-AllowHostInputProcessing-Keyboard-Mapping-Feature#Comments</comments>
      <guid>https://weblog.west-wind.com/posts/2025/Aug/20/Using-the-new-WebView2-AllowHostInputProcessing-Keyboard-Mapping-Feature</guid>
      <pubDate>Wed, 20 Aug 2025 17:17:34 GMT</pubDate>
      <abstract><![CDATA[If you've used the WebVIew2 control for a UI interactive Hybrid active you've probably run into some odd keyboard behavior, where some keys cannot be captured properly in the host application or are not forwarded quite the way they normally behave. In the newer releases of the WebView SDK there's a new option called `AllowHostInputProcessing` that allows for forwarding keyboard events more aggressively to the host which fixes some Windows behaviors and introduces some new issues. ]]></abstract>
      <featuredImage>https://weblog.west-wind.com/images/2025/Using-the-new-WebView2-AllowHostInputProcessing-Keyboard-Mapping-Feature/MenuAccellartorWebViewBanner.png</featuredImage>
    </item>
  </channel>
</rss>