1- // Papercut
1+ // Papercut
22//
33// Copyright © 2008 - 2012 Ken Robertson
44// Copyright © 2013 - 2025 Jaben Cargman
1919namespace Papercut . Service . Application . StaticContent ;
2020
2121using System . Reflection ;
22+ using System . Text . RegularExpressions ;
2223
2324public class StaticContentController : ControllerBase
2425{
@@ -44,6 +45,17 @@ public class StaticContentController : ControllerBase
4445 { "webmanifest" , "application/manifest+json" } ,
4546 } ;
4647
48+ /// <summary>
49+ /// The SPA ships with a root base href. When the service is mounted under an
50+ /// HttpPathPrefix that is wrong for every url the app builds from it, so the
51+ /// tag is rewritten on the way out to whatever prefix this request arrived on.
52+ /// </summary>
53+ static readonly Regex _baseHrefRegex = new (
54+ @"<base\s+href\s*=\s*([""'])[^""']*\1" ,
55+ RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
56+
57+ const string IndexResource = "index.html" ;
58+
4759 [ HttpGet ( "{*anything}" , Order = short . MaxValue ) ]
4860 [ ResponseCache (
4961#if DEBUG
@@ -52,7 +64,7 @@ public class StaticContentController : ControllerBase
5264 Duration = 600
5365#endif
5466 ) ]
55- public IActionResult Get ( )
67+ public async Task < IActionResult > Get ( )
5668 {
5769 var resourceName = GetRequestedResourceName ( Request . Path ) ;
5870 var resourceContent = GetResourceStream ( resourceName ) ;
@@ -61,7 +73,7 @@ public IActionResult Get()
6173 {
6274 // deep links into the Angular SPA (e.g. /message/{id}) fall back to index.html
6375 // so the client-side router can handle the route
64- resourceName = "index.html" ;
76+ resourceName = IndexResource ;
6577 resourceContent = GetResourceStream ( resourceName ) ;
6678 }
6779
@@ -70,9 +82,26 @@ public IActionResult Get()
7082 return NotFound ( ) ;
7183 }
7284
85+ if ( string . Equals ( resourceName , IndexResource , StringComparison . OrdinalIgnoreCase ) )
86+ {
87+ return Content ( await ReadIndexWithBaseHrefAsync ( resourceContent ) , "text/html" , Encoding . UTF8 ) ;
88+ }
89+
7390 return new FileStreamResult ( resourceContent , GetMimeType ( resourceName ) ) ;
7491 }
7592
93+ async Task < string > ReadIndexWithBaseHrefAsync ( Stream content )
94+ {
95+ using var reader = new StreamReader ( content , Encoding . UTF8 ) ;
96+ var html = await reader . ReadToEndAsync ( ) ;
97+
98+ // PathBase is what UsePathBase stripped off, i.e. exactly the prefix this
99+ // request came in on. Empty when there is no prefix, giving "/" as before.
100+ var baseHref = $ "{ Request . PathBase . Value ? . TrimEnd ( '/' ) } /";
101+
102+ return _baseHrefRegex . Replace ( html , match => $ "<base href=\" { baseHref } \" ", 1 ) ;
103+ }
104+
76105 static string GetRequestedResourceName ( string requestUri )
77106 {
78107 var filename = requestUri
0 commit comments