From fea01ffd2267a5468d6273e89a0657756f52ba92 Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 13 May 2026 17:26:39 +0800 Subject: [PATCH 1/4] fix: preserve URL hash fragment in buildClearURL buildClearURL now preserves the #fragment portion of the original URL when clearing query parameters, ensuring hash-based routing is not lost. Closes #11 --- internal/server/server.go | 7 ++++ internal/server/server_test.go | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/internal/server/server.go b/internal/server/server.go index bfb1255..05891c8 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -716,9 +716,16 @@ func toHomePageSessions(docs []session.DocumentInfo) []homePageSession { func buildClearURL(u *url.URL, removeKey string) string { q := u.Query() q.Del(removeKey) + fragment := u.Fragment if len(q) == 0 { + if fragment != "" { + return "/#" + fragment + } return "/" } + if fragment != "" { + return "/?" + q.Encode() + "#" + fragment + } return "/?" + q.Encode() } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 077dee3..eb5684c 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -715,3 +715,65 @@ func TestDownloadSessionNotFound(t *testing.T) { t.Fatalf("expected 404, got %d", resp.StatusCode) } } + +func TestBuildClearURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rawURL string + removeKey string + want string + }{ + { + name: "no query no fragment", + rawURL: "/", + removeKey: "q", + want: "/", + }, + { + name: "remove only param leaves root", + rawURL: "/?q=test", + removeKey: "q", + want: "/", + }, + { + name: "remove one param keeps others", + rawURL: "/?q=test&tag=foo", + removeKey: "q", + want: "/?tag=foo", + }, + { + name: "fragment preserved with no remaining params", + rawURL: "/?q=test#section", + removeKey: "q", + want: "/#section", + }, + { + name: "fragment preserved with remaining params", + rawURL: "/?q=test&tag=foo#section", + removeKey: "q", + want: "/?tag=foo#section", + }, + { + name: "fragment preserved when key not present", + rawURL: "/?tag=foo#section", + removeKey: "q", + want: "/?tag=foo#section", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + u, err := url.Parse(tt.rawURL) + if err != nil { + t.Fatal(err) + } + got := buildClearURL(u, tt.removeKey) + if got != tt.want { + t.Errorf("buildClearURL(%q, %q) = %q, want %q", tt.rawURL, tt.removeKey, got, tt.want) + } + }) + } +} From 4d07cbff4bfe4235ff8c1e39d749932a6c160f7a Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 13 May 2026 17:30:56 +0800 Subject: [PATCH 2/4] test: add fragment-only test case for buildClearURL Address review feedback: add test for URL with only fragment and no query params (/#section with removeKey=q) to verify fragment is preserved when there are no query parameters. --- internal/server/server_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/server/server_test.go b/internal/server/server_test.go index eb5684c..7e8c8d3 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -761,6 +761,12 @@ func TestBuildClearURL(t *testing.T) { removeKey: "q", want: "/?tag=foo#section", }, + { + name: "fragment only no query params", + rawURL: "/#section", + removeKey: "q", + want: "/#section", + }, } for _, tt := range tests { From 273291fe46db9ba6d5a3f621c8a3a292a6f1089e Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 13 May 2026 17:38:28 +0800 Subject: [PATCH 3/4] refactor: use url.URL struct for fragment encoding safety in buildClearURL Replace string concatenation with url.URL{}.String() to properly handle special characters in URL fragments. Add test case for special character encoding. --- internal/server/server.go | 16 +++++----------- internal/server/server_test.go | 6 ++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 05891c8..b1bb7ee 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -716,17 +716,11 @@ func toHomePageSessions(docs []session.DocumentInfo) []homePageSession { func buildClearURL(u *url.URL, removeKey string) string { q := u.Query() q.Del(removeKey) - fragment := u.Fragment - if len(q) == 0 { - if fragment != "" { - return "/#" + fragment - } - return "/" - } - if fragment != "" { - return "/?" + q.Encode() + "#" + fragment - } - return "/?" + q.Encode() + return (&url.URL{ + Path: "/", + RawQuery: q.Encode(), + Fragment: u.Fragment, + }).String() } func buildPageURL(u *url.URL, page int) string { diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 7e8c8d3..e43022c 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -767,6 +767,12 @@ func TestBuildClearURL(t *testing.T) { removeKey: "q", want: "/#section", }, + { + name: "fragment with special characters encoded safely", + rawURL: "/?q=test#sec%20tion", + removeKey: "q", + want: "/#sec%20tion", + }, } for _, tt := range tests { From 2e37196fde0013ed6b734e010bdf63330e42c57a Mon Sep 17 00:00:00 2001 From: Agent Date: Wed, 13 May 2026 17:53:50 +0800 Subject: [PATCH 4/4] refactor: use url.URL struct in buildPageURL for consistency with buildClearURL --- internal/server/server.go | 6 +++- internal/server/server_test.go | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/internal/server/server.go b/internal/server/server.go index b1bb7ee..feb35df 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -726,7 +726,11 @@ func buildClearURL(u *url.URL, removeKey string) string { func buildPageURL(u *url.URL, page int) string { q := u.Query() q.Set("page", fmt.Sprintf("%d", page)) - return "/?" + q.Encode() + return (&url.URL{ + Path: "/", + RawQuery: q.Encode(), + Fragment: u.Fragment, + }).String() } func (s *Server) handleCreateSession(w http.ResponseWriter, r *http.Request) { diff --git a/internal/server/server_test.go b/internal/server/server_test.go index e43022c..d3a3393 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -789,3 +789,59 @@ func TestBuildClearURL(t *testing.T) { }) } } + +func TestBuildPageURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rawURL string + page int + want string + }{ + { + name: "basic page", + rawURL: "/", + page: 2, + want: "/?page=2", + }, + { + name: "preserves existing params", + rawURL: "/?q=test", + page: 3, + want: "/?page=3&q=test", + }, + { + name: "preserves fragment", + rawURL: "/?q=test#section", + page: 2, + want: "/?page=2&q=test#section", + }, + { + name: "fragment only no query", + rawURL: "/#section", + page: 1, + want: "/?page=1#section", + }, + { + name: "replaces existing page param", + rawURL: "/?page=1&q=test", + page: 5, + want: "/?page=5&q=test", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + u, err := url.Parse(tt.rawURL) + if err != nil { + t.Fatal(err) + } + got := buildPageURL(u, tt.page) + if got != tt.want { + t.Errorf("buildPageURL(%q, %d) = %q, want %q", tt.rawURL, tt.page, got, tt.want) + } + }) + } +}