From 9eabd930987ce93fd90e73e207c2983ab4641df3 Mon Sep 17 00:00:00 2001 From: Duc-Tam Nguyen Date: Mon, 15 Jun 2026 22:46:42 +0700 Subject: [PATCH] Redirect / to the main page instead of serving it in place The served main page broke its own CSS and images when the entry point was a nested page. kage saves each page's asset links as mirror-relative paths (../_kage/...) computed for that page's own location, but the handler served the main page's bytes directly at /, so the browser resolved those relative URLs against / and 404ed every one of them. A developer.apple.com/documentation mirror landed at / with no styles. Redirect / to the main page's canonical content path, the way the archive's W/mainPage redirect already does, so the browser navigates to the page's real URL and resolves its relative assets correctly. Kiwix was unaffected because it follows that redirect itself. --- pack/pack_test.go | 18 ++++++++++++++++++ pack/serve.go | 16 +++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/pack/pack_test.go b/pack/pack_test.go index 448e6a9..b694e9d 100644 --- a/pack/pack_test.go +++ b/pack/pack_test.go @@ -326,6 +326,24 @@ func TestHandler(t *testing.T) { if code, body := get("/"); code != 200 || !bytes.Contains([]byte(body), []byte("Example Home")) { t.Errorf("GET / = %d %.30q", code, body) } + + // "/" must redirect to the main page's canonical content path, not serve its + // bytes in place, so the page's mirror-relative asset URLs resolve against the + // right base instead of 404ing. + noFollow := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }} + resp, err := noFollow.Get(srv.URL + "/") + if err != nil { + t.Fatal(err) + } + _ = resp.Body.Close() + if resp.StatusCode != http.StatusFound { + t.Errorf("GET / status = %d, want 302", resp.StatusCode) + } + if loc := resp.Header.Get("Location"); loc != "/index.html" { + t.Errorf("GET / Location = %q, want %q", loc, "/index.html") + } if code, _ := get("/about/index.html"); code != 200 { t.Errorf("GET /about/index.html = %d", code) } diff --git a/pack/serve.go b/pack/serve.go index ff3156c..2f4684f 100644 --- a/pack/serve.go +++ b/pack/serve.go @@ -8,14 +8,24 @@ import ( "github.com/tamnd/kage/zim" ) -// Handler serves a ZIM archive over HTTP. "/" maps to the archive's main page; -// "/a/b.png" maps to the C/a/b.png content entry. Because the saved HTML's links -// are mirror-relative paths, and those are exactly the C urls, a click in a +// Handler serves a ZIM archive over HTTP. "/" redirects to the archive's main +// page; "/a/b.png" maps to the C/a/b.png content entry. Because the saved HTML's +// links are mirror-relative paths, and those are exactly the C urls, a click in a // served page hits the right entry with no rewriting. A miss is a plain 404. func Handler(r *zim.Reader) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { p := strings.TrimPrefix(req.URL.Path, "/") if p == "" { + // The main page's saved HTML carries mirror-relative asset URLs + // (../_kage/...) computed for its own nested location, so serving its + // bytes at "/" would resolve them against the wrong base and 404 the + // page's CSS and images. Redirect to the page's canonical content path + // instead, the way the archive's W/mainPage redirect does, so the + // browser resolves those relative URLs correctly. + if ns, url, ok := r.MainPageRef(); ok && ns == zim.NamespaceContent { + http.Redirect(w, req, "/"+url, http.StatusFound) + return + } blob, err := r.MainPage() if err != nil { http.NotFound(w, req)