# All search state serialized into URL query parameters for back/forward navigation

- **Pattern:** `ab-001067` (`directory-search-discovery.pagination-url.url-state-sync`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001067
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001067)

## Why it matters

When search state lives only in client memory, the back button becomes useless, sharing a result page over Slack or email delivers the unfiltered homepage, and refreshing the tab wipes every refinement. Users expect URLs to be durable addresses — breaking that contract means every deep link from Google, Reddit, or a friend lands on the wrong state. URL-synced state is also the only reliable way to give users back/forward history through their filter exploration, which doubles as an undo stack.

## Severity rationale

Critical because URL-less state breaks sharability, bookmarks, back/forward navigation, and SEO indexing of filtered views.

## Remediation

Drive all filter, sort, and pagination state through `URLSearchParams` and `router.push`. Read initial values from `useSearchParams` on mount so the URL is the single source of truth. Edit `app/listings/search/page.tsx`:

```tsx
const updateFilters = (next) => {
  const params = new URLSearchParams(searchParams)
  Object.entries(next).forEach(([k, v]) => v ? params.set(k, v) : params.delete(k))
  router.push(`?${params}`)
}
```

## Detection

- **ID:** `url-state-sync`
- **Severity:** `critical`
- **What to look for:** Enumerate all relevant files and Perform a search, apply filters, and navigate to page 2. Check the URL — it should contain query parameters for the search term (`q=`), active filters (`category=`, `price=`, etc.), and page number (`page=`). Use the browser back button and verify the previous state is restored (including filters and page). Use forward and verify the state advances correctly.
- **Pass criteria:** No more than 0 violations are acceptable. All search state (search term, all active filters, current page) is encoded in the URL query parameters. Browser back/forward buttons correctly restore and advance through the browsing history.
- **Fail criteria:** URL does not update with filter/page changes, or back/forward buttons do not restore state, or search state is stored in local storage/client-side state without URL sync.
- **Skip (N/A) when:** Search/filtering is not implemented. Signal: no search input or filters found.
- **Detail on fail:** `"URL does not update when filters are applied. Filters are stored in client state only. Back button does not work to undo filter changes."` or `"Page parameter is in URL but filter parameters are missing."`
- **Remediation:** Sync all state to URL query parameters:

  ```tsx
  // pages/listings/search.tsx
  import { useSearchParams } from 'next/navigation'
  import { useRouter } from 'next/navigation'

  export default function SearchPage() {
    const searchParams = useSearchParams()
    const router = useRouter()
    const [filters, setFilters] = useState({
      q: searchParams.get('q') || '',
      category: searchParams.get('category') || '',
      page: parseInt(searchParams.get('page') || '1')
    })

    const updateFilters = (newFilters) => {
      const params = new URLSearchParams(searchParams)
      Object.entries(newFilters).forEach(([k, v]) => {
        if (v) params.set(k, v)
        else params.delete(k)
      })
      router.push(`?${params}`)
    }

    return (
      <div>
        <SearchInput value={filters.q} onChange={q => updateFilters({ ...filters, q })} />
        <FilterPanel filters={filters} onChange={f => updateFilters(f)} />
        <Pagination page={filters.page} onChange={p => updateFilters({ ...filters, page: p })} />
      </div>
    )
  }
  ```

Taxons: user-experience

HTML version: https://auditbuffet.com/patterns/ab-001067
