ソースを参照

Fix docs page behind a reverse proxy (#304)

DocsHomePage fetched its own static docs over HTTP through an
HttpClient whose BaseAddress was NavigationManager.BaseUri — the
browser-facing URL. Inside the Docker container that URL is often
unreachable (a proxy hostname the container can't resolve, or the
host-mapped port), so the docs index and pages failed to load.

- Introduce IDocsContentProvider: the Blazor Server host reads docs
  from the composed static web assets on disk
  (StaticWebAssetDocsContentProvider); the WASM viewer keeps fetching
  over HTTP relative to the app base (HttpDocsContentProvider).
- Remove the nav-based HttpClient registration from the server host
  (DocsHomePage was its only consumer).
- Register Shared.Rcl with MapRazorComponents via
  AddAdditionalAssemblies: RCL pages previously had no server-side
  endpoints and returned HTTP 404 (with interactively rendered
  content) — the '404 but has content' noted in the issue. All pages
  now return 200.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tim Jones 18 時間 前
コミット
940fd4a474

+ 2 - 0
RackPeek.Web.Viewer/Program.cs

@@ -6,6 +6,7 @@ using RackPeek.Domain.Git;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Persistence.Yaml;
 using RackPeek.Domain.Persistence.Yaml;
 using Shared.Rcl;
 using Shared.Rcl;
+using Shared.Rcl.Docs;
 
 
 namespace RackPeek.Web.Viewer;
 namespace RackPeek.Web.Viewer;
 
 
@@ -51,6 +52,7 @@ public class Program {
         builder.Services.AddUseCases();
         builder.Services.AddUseCases();
 
 
         builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
         builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+        builder.Services.AddScoped<IDocsContentProvider, HttpDocsContentProvider>();
 
 
         await builder.Build().RunAsync();
         await builder.Build().RunAsync();
     }
     }

+ 5 - 7
RackPeek.Web/Program.cs

@@ -8,6 +8,8 @@ using RackPeek.Domain.Persistence.Yaml;
 using RackPeek.Web.Api;
 using RackPeek.Web.Api;
 using RackPeek.Web.Components;
 using RackPeek.Web.Components;
 using Shared.Rcl;
 using Shared.Rcl;
+using Shared.Rcl.Docs;
+using Shared.Rcl.Servers;
 
 
 namespace RackPeek.Web;
 namespace RackPeek.Web;
 
 
@@ -53,12 +55,7 @@ public class Program {
         });
         });
         builder.Services.AddScoped<ITextFileStore, PhysicalTextFileStore>();
         builder.Services.AddScoped<ITextFileStore, PhysicalTextFileStore>();
 
 
-        builder.Services.AddScoped(sp => {
-            NavigationManager nav = sp.GetRequiredService<NavigationManager>();
-            return new HttpClient {
-                BaseAddress = new Uri(nav.BaseUri)
-            };
-        });
+        builder.Services.AddScoped<IDocsContentProvider, StaticWebAssetDocsContentProvider>();
 
 
         builder.Services.AddGitServices(builder.Configuration, yamlPath);
         builder.Services.AddGitServices(builder.Configuration, yamlPath);
 
 
@@ -102,7 +99,8 @@ public class Program {
         app.MapStaticAssets();
         app.MapStaticAssets();
 
 
         app.MapRazorComponents<App>()
         app.MapRazorComponents<App>()
-            .AddInteractiveServerRenderMode();
+            .AddInteractiveServerRenderMode()
+            .AddAdditionalAssemblies(typeof(ServersListPage).Assembly);
 
 
         return app;
         return app;
     }
     }

+ 25 - 0
RackPeek.Web/StaticWebAssetDocsContentProvider.cs

@@ -0,0 +1,25 @@
+using Microsoft.Extensions.FileProviders;
+using Shared.Rcl.Docs;
+
+namespace RackPeek.Web;
+
+/// <summary>
+///     Reads the docs directly from the composed static web assets
+///     (Shared.Rcl's wwwroot in development, wwwroot/_content/Shared.Rcl in a
+///     published app), so the server never has to call back to its own public
+///     URL — which is unreachable from inside the container behind a reverse
+///     proxy (issue #304).
+/// </summary>
+public class StaticWebAssetDocsContentProvider(IWebHostEnvironment environment) : IDocsContentProvider {
+    public async Task<string> GetAsync(string fileName) {
+        IFileInfo file = environment.WebRootFileProvider
+            .GetFileInfo($"_content/Shared.Rcl/raw_docs/{fileName}");
+
+        if (!file.Exists)
+            throw new FileNotFoundException($"Docs file '{fileName}' not found.");
+
+        await using Stream stream = file.CreateReadStream();
+        using var reader = new StreamReader(stream);
+        return await reader.ReadToEndAsync();
+    }
+}

+ 5 - 4
Shared.Rcl/Docs/DocsHomePage.razor

@@ -1,10 +1,11 @@
 @page "/docs"
 @page "/docs"
 @page "/docs/{*Page}"
 @page "/docs/{*Page}"
 
 
-@inject HttpClient Http
+@inject IDocsContentProvider Docs
 @inject NavigationManager Nav
 @inject NavigationManager Nav
 @inject IJSRuntime JS
 @inject IJSRuntime JS
 @using Markdig
 @using Markdig
+@using System.Text.Json
 @implements IDisposable
 @implements IDisposable
 
 
 <PageTitle>Docs@(!string.IsNullOrWhiteSpace(_activeTitle) ? $": {_activeTitle}" : "")</PageTitle>
 <PageTitle>Docs@(!string.IsNullOrWhiteSpace(_activeTitle) ? $": {_activeTitle}" : "")</PageTitle>
@@ -164,7 +165,7 @@
             var url = $"_content/Shared.Rcl/raw_docs/{decoded}";
             var url = $"_content/Shared.Rcl/raw_docs/{decoded}";
             _lastFetchUrl = url;
             _lastFetchUrl = url;
 
 
-            var markdown = await Http.GetStringAsync(url);
+            var markdown = await Docs.GetAsync(decoded);
             _htmlContent = Markdown.ToHtml(markdown, Pipeline);
             _htmlContent = Markdown.ToHtml(markdown, Pipeline);
 
 
             _activeTitle = DisplayName(decoded);
             _activeTitle = DisplayName(decoded);
@@ -215,8 +216,8 @@
 
 
         try
         try
         {
         {
-            var url = "_content/Shared.Rcl/raw_docs/docs-index.json";
-            var items = await Http.GetFromJsonAsync<List<string>>(url);
+            var json = await Docs.GetAsync("docs-index.json");
+            var items = JsonSerializer.Deserialize<List<string>>(json);
 
 
             _docsIndex = (items ?? new List<string>())
             _docsIndex = (items ?? new List<string>())
                 .Where(x => !string.IsNullOrWhiteSpace(x))
                 .Where(x => !string.IsNullOrWhiteSpace(x))

+ 6 - 0
Shared.Rcl/Docs/HttpDocsContentProvider.cs

@@ -0,0 +1,6 @@
+namespace Shared.Rcl.Docs;
+
+public class HttpDocsContentProvider(HttpClient http) : IDocsContentProvider {
+    public Task<string> GetAsync(string fileName) =>
+        http.GetStringAsync($"_content/Shared.Rcl/raw_docs/{fileName}");
+}

+ 17 - 0
Shared.Rcl/Docs/IDocsContentProvider.cs

@@ -0,0 +1,17 @@
+namespace Shared.Rcl.Docs;
+
+/// <summary>
+///     Supplies the raw documentation assets shipped under
+///     _content/Shared.Rcl/raw_docs. The Blazor Server host reads them from
+///     the static web assets on disk; fetching them over HTTP would require
+///     the container to reach its own public URL, which fails behind a
+///     reverse proxy (issue #304). The WebAssembly viewer fetches them over
+///     HTTP relative to the app base, which is always reachable there.
+/// </summary>
+public interface IDocsContentProvider {
+    /// <summary>
+    ///     Returns the content of a file under raw_docs (e.g. "overview.md",
+    ///     "docs-index.json"). Throws if the file does not exist.
+    /// </summary>
+    Task<string> GetAsync(string fileName);
+}