diff --git a/pygitweb/actions.py b/pygitweb/actions.py
index 1b95dc2..6cbbc20 100644
--- a/pygitweb/actions.py
+++ b/pygitweb/actions.py
@@ -11,7 +11,7 @@ import os
 from datetime import UTC, datetime, timedelta, timezone
 from pathlib import Path
 from typing import Any, TypedDict
-from urllib.parse import quote, urlencode
+from urllib.parse import parse_qsl, quote, urlencode
 
 import pygit2
 from fastapi import HTTPException, Request
@@ -119,7 +119,7 @@ def _parse_search_sort(value: str | None) -> PySortMode | None:
 
 def _pagination_url(request: Request, page: int, pagecount: int) -> str:
 	"""Build URL for a pagination page, preserving path and other query params."""
-	params = dict(request.query_params)
+	params = dict(parse_qsl(request.url.query, keep_blank_values=True))
 	params["page"] = str(page)
 	params["pagecount"] = str(pagecount)
 	return f"{request.url.path}?{urlencode(params)}"
@@ -412,25 +412,32 @@ def git_object(project: str, h: str | None) -> Response:
 	raise HTTPException(status_code=404, detail="Unknown object type")
 
 
-def git_search(project: str, request: Request) -> JSONResponse:
-	qp = request.query_params
-	patterns = _split_query_list(qp.getlist("patterns"))
+def git_search(
+	project: str,
+	search_patterns: str | None,
+	search_paths: str | None,
+	search_globs: str | None,
+	search_heading: str | None,
+	search_multiline: str | None,
+	search_sort: str | None,
+	search_max_count: str | None,
+) -> JSONResponse:
+	patterns = _split_query_list([search_patterns] if search_patterns is not None else [])
 	if not patterns:
 		raise HTTPException(status_code=400, detail="patterns is required (repeat ?patterns= or use comma-separated)")
 
 	project_root = _project_worktree_root(project)
-	paths = _validated_search_paths(project_root, _split_query_list(qp.getlist("paths")))
+	paths = _validated_search_paths(project_root, _split_query_list([search_paths] if search_paths is not None else []))
 
-	globs = _split_query_list(qp.getlist("globs"))
-	heading = _parse_query_bool(qp.get("heading"))
-	multiline = _parse_query_bool(qp.get("multiline"))
-	sort = _parse_search_sort(qp.get("sort"))
+	globs = _split_query_list([search_globs] if search_globs is not None else [])
+	heading = _parse_query_bool(search_heading)
+	multiline = _parse_query_bool(search_multiline)
+	sort = _parse_search_sort(search_sort)
 
-	max_count_raw = qp.get("max_count")
 	max_count: int = 25
-	if max_count_raw is not None and max_count_raw.strip() != "":
+	if search_max_count is not None and search_max_count.strip() != "":
 		try:
-			max_count = int(max_count_raw)
+			max_count = int(search_max_count)
 		except ValueError as e:
 			raise HTTPException(status_code=400, detail="max_count must be an integer") from e
 		if max_count < 1:
@@ -463,7 +470,7 @@ def git_search(project: str, request: Request) -> JSONResponse:
 		"paths": rel_paths,
 		"globs": globs,
 		"heading": heading,
-		"sort": (qp.get("sort") or None),
+		"sort": (search_sort or None),
 		"max_count": max_count,
 		"multiline": multiline,
 		"results": results,
diff --git a/pygitweb/main.py b/pygitweb/main.py
index a7ab708..01494b8 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -15,7 +15,7 @@ from contextlib import asynccontextmanager
 from datetime import UTC, datetime
 from pathlib import Path
 from typing import Annotated
-from urllib.parse import quote
+from urllib.parse import quote, urlencode
 
 import pygit2
 from fastapi import Depends, FastAPI, File, Form, HTTPException, Query, Request, UploadFile
@@ -323,7 +323,6 @@ def git_project_index(
 
 @app.get("/activity", response_class=HTMLResponse)
 def activity_page(
-	request: Request,
 	page: Annotated[str | None, Query(alias="page")] = None,
 	pagecount: Annotated[str | None, Query(alias="pagecount")] = None,
 ) -> HTMLResponse:
@@ -371,9 +370,9 @@ def activity_page(
 	prev_url = ""
 	next_url = ""
 	if p > 1:
-		prev_url = f"{request.url.path}?page={p - 1}&pagecount={pc}"
+		prev_url = f"/activity?{urlencode({'page': str(p - 1), 'pagecount': str(pc)})}"
 	if has_next:
-		next_url = f"{request.url.path}?page={p + 1}&pagecount={pc}"
+		next_url = f"/activity?{urlencode({'page': str(p + 1), 'pagecount': str(pc)})}"
 	pagination_html = env.get_template("pagination.html").render(
 		current_page=p,
 		pagecount=pc,
@@ -424,8 +423,10 @@ _OPTIONAL_REPO_ZIP = File(default=None)
 
 
 @app.get("/projectnamevalid", response_class=HTMLResponse)
-def addproject_namevalid(name: Annotated[str, Query()]):
+def addproject_namevalid(name: Annotated[str | None, Query()] = None):
 	"""Check if project name is valid."""
+	if not name:
+		raise HTTPException(status_code=400, detail="Param 'name' required")
 	if not is_valid_pathname(name):
 		raise HTTPException(status_code=400, detail="Invalid project name (no path segments)")
 	if project_visible_in_list(name):
@@ -732,6 +733,13 @@ async def dispatch(
 	pagecount: Annotated[str | None, Query(alias="pagecount")] = None,
 	updates: Annotated[str | None, Query(alias="updates")] = None,
 	pf: Annotated[str | None, Query(alias="pf")] = None,
+	search_patterns: Annotated[str | None, Query(alias="patterns")] = None,
+	search_paths: Annotated[str | None, Query(alias="paths")] = None,
+	search_globs: Annotated[str | None, Query(alias="globs")] = None,
+	search_heading: Annotated[str | None, Query(alias="heading")] = None,
+	search_multiline: Annotated[str | None, Query(alias="multiline")] = None,
+	search_sort: Annotated[str | None, Query(alias="sort")] = None,
+	search_max_count: Annotated[str | None, Query(alias="max_count")] = None,
 ):
 	"""
 	Dispatch by path: /project/{project} -> summary; /project/{project}/action/... -> action.
@@ -824,7 +832,16 @@ async def dispatch(
 	if action == "object":
 		return git_object(project, hash_param)
 	if action == "search":
-		return git_search(project, request)
+		return git_search(
+			project,
+			search_patterns,
+			search_paths,
+			search_globs,
+			search_heading,
+			search_multiline,
+			search_sort,
+			search_max_count,
+		)
 	# Stub others with minimal response
 	pre = PREAMBLE.render(title=f"{action} - {project}", site_name=settings.SITE_NAME)
 	return HTMLResponse(f"{pre}<p>Action: {jinja_escape(action)}</p><p>Project: {jinja_escape(project)}</p>{POSTAMBLE}")
