Skip to content

fix: preserve ?search and ?section through URL normalization redirect#418

Merged
tomusdrw merged 2 commits intomainfrom
td-fix-search-url
Apr 27, 2026
Merged

fix: preserve ?search and ?section through URL normalization redirect#418
tomusdrw merged 2 commits intomainfrom
td-fix-search-url

Conversation

@tomusdrw
Copy link
Copy Markdown
Member

@tomusdrw tomusdrw commented Apr 27, 2026

Summary

  • When loading a URL like /#/?search=foo&section=bar (no version segment), the LocationProvider redirect to /#/<hash>?v=<name> was stripping ?search/?section and a second hashchange immediately wiped them from state before Search and Outline could consume them. The redirect now opts in to keeping those fields via a new includeSearchSection flag on locationParamsToHash, so the rewritten URL still carries them and the second pass restores the same state.
  • All other call sites (Version, SelectionProvider, NoteManager, SplitScreenProvider) keep using the default serialization, so subsequent navigation still drops ?search/?section and preserves the existing "consume once" semantics — version changes and manual navigation to a plain hash correctly clear them.
  • New tools/snapshot-tests/tests/url-params.spec.ts covers the original bug for both URL forms (with/without a version segment), the combined search+section case, and the regression scenario where a follow-up hash change must drop the fields.

Test plan

  • npm test (unit tests, 96 passing)
  • npx biome ci clean
  • npx playwright test url-params.spec.ts (10/10 across light + dark mode)
  • Manual smoke: open /#/?search=protocol&section=Header and confirm input is filled and the viewer scrolls; then change version and confirm the URL no longer carries those params

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed an issue where search and section URL parameters were lost during subsequent navigation events, ensuring these parameters persist correctly in the URL.
  • Tests

    • Added comprehensive tests for URL query parameter parsing and handling, verifying correct behavior of search and section parameters across various navigation scenarios.

…rect

When opening a URL like /#/?search=foo&section=bar (no version segment), the
LocationProvider redirects to /#/<latest-hash>?v=<name> and the resulting
hashchange used to fire a second handleHashChange that read undefined for
both fields and wiped state before Search/Outline could consume it. The
redirect now opts in to keeping ?search/?section in the rewritten URL via
locationParamsToHash's new includeSearchSection flag, so the second pass
reads the original values and state survives. Subsequent navigation goes
through the default path and still drops these fields, matching the
existing "consume once" semantics. Adds e2e coverage in
tools/snapshot-tests/tests/url-params.spec.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 27, 2026

Deploy Preview for graypaper-reader ready!

Name Link
🔨 Latest commit 022128e
🔍 Latest deploy log https://app.netlify.com/projects/graypaper-reader/deploys/69ef58f56fadee000873b7f4
😎 Deploy Preview https://deploy-preview-418--graypaper-reader.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (4)
  • tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-header-options-menu-dark-mode-linux.png is excluded by !**/*.png
  • tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-header-version-dropdown-light-mode-linux.png is excluded by !**/*.png
  • tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-sidebar-overlay-closed-dark-mode-linux.png is excluded by !**/*.png
  • tools/snapshot-tests/tests/split-screen.spec.ts-snapshots/split-sidebar-overlay-dark-mode-linux.png is excluded by !**/*.png

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9c52add3-9a01-4bb2-af9d-900323b05d0e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR modifies URL parameter handling in LocationProvider to preserve ?search and ?section query parameters during hash canonicalization. The locationParamsToHash function now accepts an includeSearchSection flag to conditionally include these parameters in serialized URLs, while handleHashChange redirects by rewriting the hash directly instead of calling handleSetLocationParams.

Changes

Cohort / File(s) Summary
LocationProvider hash handling
src/components/LocationProvider/LocationProvider.tsx, src/components/LocationProvider/utils/locationParamsToHash.ts
Modified handleHashChange to redirect via window.location.hash rewrite using locationParamsToHash(..., { includeSearchSection: true }) in canonicalization branches, preventing loss of ?search and ?section during subsequent hashchange events. Updated locationParamsToHash function signature to accept optional options parameter with includeSearchSection flag that conditionally includes search/section params in serialized URL.
URL parameter tests
tools/snapshot-tests/tests/url-params.spec.ts
New Playwright test suite validating SPA URL query parsing for ?search and ?section parameters across various navigation scenarios, including version segments, PDF UI readiness, search input population, PDF viewer scrolling behavior, and in-session hash updates that verify parameter handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • DrEverr
  • mateuszsikora

Poem

🐰 A hash that lost its search and section,
Now rewritten with great affection!
With includeSearchSection flag so true,
URL params hop back into view. 🔗✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: preserving ?search and ?section parameters through URL normalization redirects in LocationProvider.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch td-fix-search-url

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/components/LocationProvider/LocationProvider.tsx (1)

86-114: Refactor duplicated canonical redirect block into a local helper.

The redirect-hash construction appears twice with identical logic. Extracting it avoids branch drift in future edits.

♻️ Suggested deduplication
   const handleHashChange = useCallback(() => {
     const { rest: newHash, search, section, split } = extractSearchParams(window.location.hash);

     const resolvedSplit = split ? (resolveFullVersion(split) ?? undefined) : undefined;
+    const rewriteCanonicalHash = (version: string) => {
+      const redirectHash = locationParamsToHash({ version, search, section, split: resolvedSplit }, metadata, {
+        includeSearchSection: true,
+      });
+      window.location.hash = redirectHash;
+    };

     if (!newHash.startsWith(SEGMENT_SEPARATOR)) {
       const version = metadata.latest;
       setLocationParams((params) => ({
         ...params,
         version,
         search,
         section,
         split: resolvedSplit,
       }));
-      const redirectHash = locationParamsToHash({ version, search, section, split: resolvedSplit }, metadata, {
-        includeSearchSection: true,
-      });
-      window.location.hash = redirectHash;
+      rewriteCanonicalHash(version);
       return;
     }
@@
     if (!fullVersion) {
       const version = metadata.latest;
       setLocationParams((params) => ({
         ...params,
         version,
         search,
         section,
         split: resolvedSplit,
       }));
-      const redirectHash = locationParamsToHash({ version, search, section, split: resolvedSplit }, metadata, {
-        includeSearchSection: true,
-      });
-      window.location.hash = redirectHash;
+      rewriteCanonicalHash(version);
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/LocationProvider/LocationProvider.tsx` around lines 86 - 114,
There are two identical blocks that build a canonical redirect hash and set
state/window.location.hash; extract them into a local helper (e.g.,
redirectToCanonical) inside LocationProvider.tsx that accepts a version (or uses
metadata.latest when needed), calls setLocationParams with { version, search,
section, split: resolvedSplit }, constructs the redirectHash via
locationParamsToHash(..., metadata, { includeSearchSection: true }), and sets
window.location.hash = redirectHash; then replace both duplicated blocks with a
call to this helper (use resolveFullVersion when computing the version before
calling it).
tools/snapshot-tests/tests/url-params.spec.ts (1)

11-15: Avoid duplicated selector literals in getScrollTop.

Reuse PDF_SCROLL_CONTAINER via page.evaluate arg to prevent selector drift.

🧩 Small maintainability tweak
 async function getScrollTop(page: import("@playwright/test").Page) {
-  return await page.evaluate(() => {
-    const viewer = document.querySelector(".pdfViewer");
+  return await page.evaluate((selector) => {
+    const viewer = document.querySelector(selector);
     return viewer?.parentElement?.scrollTop ?? 0;
-  });
+  }, PDF_SCROLL_CONTAINER);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/snapshot-tests/tests/url-params.spec.ts` around lines 11 - 15, The
getScrollTop function duplicates the ".pdfViewer" selector literal; change it to
accept/use the shared PDF_SCROLL_CONTAINER constant by passing it into
page.evaluate as an argument (e.g., call page.evaluate((sel) => { const viewer =
document.querySelector(sel); ... }, PDF_SCROLL_CONTAINER)) so the selector is
not hard-coded inside the evaluated function; update getScrollTop to reference
PDF_SCROLL_CONTAINER and remove the inline string literal to prevent selector
drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/LocationProvider/LocationProvider.tsx`:
- Around line 86-114: There are two identical blocks that build a canonical
redirect hash and set state/window.location.hash; extract them into a local
helper (e.g., redirectToCanonical) inside LocationProvider.tsx that accepts a
version (or uses metadata.latest when needed), calls setLocationParams with {
version, search, section, split: resolvedSplit }, constructs the redirectHash
via locationParamsToHash(..., metadata, { includeSearchSection: true }), and
sets window.location.hash = redirectHash; then replace both duplicated blocks
with a call to this helper (use resolveFullVersion when computing the version
before calling it).

In `@tools/snapshot-tests/tests/url-params.spec.ts`:
- Around line 11-15: The getScrollTop function duplicates the ".pdfViewer"
selector literal; change it to accept/use the shared PDF_SCROLL_CONTAINER
constant by passing it into page.evaluate as an argument (e.g., call
page.evaluate((sel) => { const viewer = document.querySelector(sel); ... },
PDF_SCROLL_CONTAINER)) so the selector is not hard-coded inside the evaluated
function; update getScrollTop to reference PDF_SCROLL_CONTAINER and remove the
inline string literal to prevent selector drift.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 902779ec-e270-4085-8dba-f5f13a013912

📥 Commits

Reviewing files that changed from the base of the PR and between 7db5c20 and 87da5a0.

📒 Files selected for processing (3)
  • src/components/LocationProvider/LocationProvider.tsx
  • src/components/LocationProvider/utils/locationParamsToHash.ts
  • tools/snapshot-tests/tests/url-params.spec.ts

@github-actions
Copy link
Copy Markdown
Contributor

Visual Regression Test Report ✅ Passed

Github run id: 24995292162

🔗 Artifacts: Download

@tomusdrw tomusdrw merged commit ee47526 into main Apr 27, 2026
5 checks passed
@tomusdrw tomusdrw deleted the td-fix-search-url branch April 27, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant