Search Results Page Design
Frontend system design for search results — query UX, filters, ranking display, perf, SEO, and empty states.
- system-design
- interview
- architecture
- search
Scope the problem
In scope: search results page (SRP) frontend — query box, results list, facets/filters, pagination/infinite scroll, performance, SEO for public search.
Out of scope: inverted index internals / ML ranker training.
Critical journey
Type query → intent suggestions → submit → results + facets → refine → open item
Architecture
SearchPage
├── SearchHeader (query, suggestions)
├── FacetSidebar / chips
├── ResultsMeta (count, sort, latency)
├── ResultsList (virtualized)
└── Pagination or infinite sentinel
URL is source of truth:
/search?q=running+shoes&brand=nike&page=2&sort=price_asc
Shareable, refresh-safe, back-button friendly.
Data contracts
type SearchRequest = {
q: string;
filters: Record<string, string[]>;
sort: string;
cursor?: string;
page?: number;
};
type SearchHit = {
id: string;
title: string;
snippetHtml: string; // pre-escaped highlights from server
imageUrl?: string;
url: string;
};
type SearchResponse = {
hits: SearchHit[];
total: number;
facets: { key: string; values: { value: string; count: number }[] }[];
nextCursor?: string | null;
correctedQuery?: string; // did-you-mean
};
Query UX
- Debounced typeahead suggestions (autocomplete) separate from full search submit
- Enter submits full SRP navigation
- Cancel in-flight searches on query change (AbortController)
- Preserve filters when query changes? Product decision — usually reset facets
Filters & facets
- Multi-select facets with counts
- Apply: update URL → fetch
- Mobile: filters in drawer
- Show active chips with remove
Facet counts should reflect other selected filters (server responsibility); client just renders.
Rendering results
- Highlight snippets: server-sanitized HTML only
- Image lazy load; reserve aspect
- Skeleton on first load; keep previous results with opacity on refine (placeholderData)
- Empty: suggestions, clear filters, popular queries
- Zero-result after typo: show
correctedQuerybanner
Pagination vs infinite scroll
| Mode | Pros | Cons |
|---|---|---|
| Page numbers | SEO, jump, stable analytics | extra clicks |
| Infinite | engagement | hard to deep-link item #80; SEO weaker |
Public web search often pages; app search may infinite. Hybrid: infinite with ?page= updates.
Performance
- Critical CSS for header + first results
- Virtualize long lists
- Prefetch result detail on hover
- Cache recent queries in React Query (
staleTimeshort) - Edge cache anonymous popular queries carefully (personalization none)
SEO (public SRP)
- Server-render first page of results
- Unique titles
Search results for "…" - Rel next/prev if paginated
- Avoid infinite-only crawl traps
- Noindex ultra-thin query pages if policy requires
Analytics
search_submit,search_result_click(position, id)- Zero-result rate
- Filter usage
A11y
- Results count announced politely on update
- Landmarks; skip to results
- Facets as fieldsets
- Keyboard operable chips
Tradeoffs
- Client SPA transitions vs full document for SEO
- Facet-heavy UI vs simple search
- Offset vs cursor pagination (cursor preferred under mutability)
- Personalized rank vs cacheability
Interview close
URL-driven q/filters → abortable search API → facets + chips → results with skeletons and empty/did-you-mean → pagination choice for SEO → perf virtualize + cache. Mention snippet sanitization.
Latency UX
Search feels slow not only from the engine but from serial client work. Parallelize:
- Fire results + facet query together (or one combined response)
- Render header chrome immediately
- Stream or skeleton the list
Show a subtle “Updated” state when refining filters so users know the UI heard them even if results take 400ms.
Ranking transparency (light)
For some products, explain sort (“Best match”, “Newest”, “Price”). Avoid fake precision (“99% match”) unless real. Sponsored results must be labeled for trust and compliance.
Client cache policy
// short staleTime — inventory/prices shift
staleTime: 30_000,
// keep previous while fetching next query
placeholderData: keepPreviousData,
Invalidate on login if results are personalized.
Accessibility extras
Move focus to results summary on submit (aria-live polite count). Ensure infinite scroll has a keyboard “Load more” alternative.
Related on this site
- Autocomplete Search Box
- Product Filters Sidebar
- Pagination Component
- System Design Interview Framework