Filter URLs enable shareable, bookmarkable search results. Well-designed filter parameters improve user experience and can boost SEO when implemented correctly.
Key Takeaways
- 1Use clear, descriptive parameter names
- 2Support multiple values for the same filter
- 3Handle range filters with min/max or dash notation
- 4Reset to page 1 when filters change
- 5Consider canonical URLs for SEO on filtered pages
“Faceted search enables users to refine their search results by multiple attributes simultaneously, with the URL reflecting the current filter state for easy sharing.”
Filter URL Patterns
There are several ways to encode filter values in URLs. Your choice affects URL readability, implementation complexity, and how well your URLs work across different systems.
The table below compares common patterns for representing filter values. Each has trade-offs in terms of readability and parsing complexity.
| Filter Type | URL Pattern | Example |
|---|---|---|
| Single value | ?color=blue | One option selected |
| Multiple values | ?color=blue&color=red | Multiple options |
| Array syntax | ?color[]=blue&color[]=red | Explicit array |
| Comma-separated | ?color=blue,red | Compact format |
| Range | ?price=50-100 | Min-max range |
| Min/Max | ?price_min=50&price_max=100 | Explicit range |
Multiple value patterns (repeated params, array syntax, or comma-separated) let users select more than one option for the same filter. Range patterns handle price sliders and date pickers elegantly.
Parsing Filter URLs
When a user lands on a filtered page, you need to extract filter values from the URL and apply them to your data query. Here is a robust parser that handles all common patterns.
// Parse filter parameters from URL
function parseFilters(url) {
const params = new URLSearchParams(url.search);
const filters = {};
for (const [key, value] of params.entries()) {
// Handle array notation: color[]
const cleanKey = key.replace(/\[\]$/, '');
// Handle range notation: 50-100
const rangeMatch = value.match(/^(\d+)-(\d+)$/);
if (rangeMatch) {
filters[cleanKey] = {
min: parseInt(rangeMatch[1]),
max: parseInt(rangeMatch[2])
};
continue;
}
// Handle comma-separated values
if (value.includes(',')) {
filters[cleanKey] = value.split(',');
continue;
}
// Handle multiple params with same key
if (filters[cleanKey]) {
if (!Array.isArray(filters[cleanKey])) {
filters[cleanKey] = [filters[cleanKey]];
}
filters[cleanKey].push(value);
} else {
filters[cleanKey] = value;
}
}
return filters;
}
// Example
const url = new URL('https://shop.com/products?color=blue&color=red&price=50-100&sort=newest');
const filters = parseFilters(url);
// {
// color: ['blue', 'red'],
// price: { min: 50, max: 100 },
// sort: 'newest'
// }This parser handles array notation (color[]), range notation (50-100), comma-separated values, and repeated parameters. The result is a clean object you can pass directly to your data layer.
Parsing URLs is only half the equation. You also need to build URLs from filter state when users interact with your filter UI.
Building Filter URLs
When users click filter options, you need to update the URL to reflect their selection. The URL should update without a page reload, and toggling a filter on and off should work intuitively.
// Build filter URL from state
function buildFilterUrl(baseUrl, filters) {
const url = new URL(baseUrl);
// Clear existing query
url.search = '';
Object.entries(filters).forEach(([key, value]) => {
if (value === null || value === undefined || value === '') {
return;
}
// Range filter
if (typeof value === 'object' && 'min' in value) {
if (value.min !== undefined && value.max !== undefined) {
url.searchParams.set(key, `${value.min}-${value.max}`);
}
return;
}
// Array of values
if (Array.isArray(value)) {
value.forEach(v => url.searchParams.append(key, v));
return;
}
// Single value
url.searchParams.set(key, value);
});
return url.toString();
}
// Toggle a filter value
function toggleFilter(currentUrl, key, value) {
const url = new URL(currentUrl);
const values = url.searchParams.getAll(key);
// Reset pagination when changing filters
url.searchParams.delete('page');
if (values.includes(value)) {
// Remove value
url.searchParams.delete(key);
values.filter(v => v !== value).forEach(v => {
url.searchParams.append(key, v);
});
} else {
// Add value
url.searchParams.append(key, value);
}
return url.toString();
}The toggleFilter function adds or removes individual filter values while preserving other parameters. Notice that it resets pagination to page 1 when filters change—otherwise users might land on an empty page.
Faceted navigation takes filtering further by showing users which options are available and how many results each filter produces. This helps users make informed choices.
Faceted Navigation
Faceted search shows available filter options with result counts, helping users narrow down results without hitting dead ends. Your URL structure needs to support both query-based and path-based approaches.
// Faceted navigation with counts
// URL: /products?category=shoes&brand=nike&color=blue
function buildFacetedUrl(basePath, facets) {
// Option 1: Query parameters (most common)
const params = new URLSearchParams();
Object.entries(facets).forEach(([key, values]) => {
values.forEach(v => params.append(key, v));
});
return `${basePath}?${params}`;
}
// Option 2: Path-based facets (SEO-friendly)
// /products/shoes/nike/blue
function buildFacetedPath(basePath, facets) {
const segments = Object.values(facets).flat();
return `${basePath}/${segments.join('/')}`;
}
// Option 3: Hybrid approach
// /products/shoes?brand=nike&color=blue
// Primary category in path, secondary filters in query
// Response with facet counts
{
"products": [...],
"facets": {
"brand": [
{ "value": "nike", "count": 45, "selected": true },
{ "value": "adidas", "count": 32, "selected": false }
],
"color": [
{ "value": "blue", "count": 15, "selected": true },
{ "value": "red", "count": 28, "selected": false }
]
}
}The response includes facet data with counts, letting your UI show options like "Nike (45)" or "Blue (15)". This helps users understand the impact of their filter choices before clicking.
Faceted navigation creates potentially thousands of URL combinations. Managing how search engines handle these URLs is critical for SEO.
SEO Considerations
Every filter combination creates a unique URL, which can lead to duplicate content and crawl budget issues. You need a strategy to tell search engines which filtered pages are worth indexing.
<!-- Filtered page SEO handling -->
<!-- Option 1: Canonical to unfiltered page -->
<!-- Best for most filter combinations -->
<link rel="canonical" href="https://shop.com/shoes" />
<!-- Option 2: Self-referencing canonical -->
<!-- For important filter pages you want indexed -->
<link rel="canonical" href="https://shop.com/shoes?brand=nike" />
<!-- Option 3: noindex filtered pages -->
<meta name="robots" content="noindex, follow" />
<!-- Recommended: Let Google decide which filters to index -->
<!-- Use Google Search Console URL Parameters tool -->
<!-- Avoid these SEO issues: -->
<!-- 1. Thousands of indexable filter combinations -->
<!-- 2. Duplicate content from different filter orders -->
<!-- 3. Thin content pages with few products -->
<!-- Sort order should NOT create different URLs -->
<!-- Use session/cookie for sort preference -->
<!-- Or use canonical if sort is in URL -->For most filter combinations, canonicalize to the unfiltered category page. Only index strategically important filter pages like branded category pages (/shoes/nike) that have real search demand.
Finally, you need to wire up your URL parsing and building to your application state. Here is a React hook pattern that keeps filter state in sync with the URL.
Filter State Management
In React applications, you want filter state to live in the URL so users can bookmark, share, and use browser navigation. This hook pattern reads from and writes to the URL seamlessly.
// React hook for URL filter state
import { useSearchParams } from 'next/navigation';
function useFilterState() {
const searchParams = useSearchParams();
const filters = {
category: searchParams.get('category'),
brand: searchParams.getAll('brand'),
priceMin: searchParams.get('price_min'),
priceMax: searchParams.get('price_max'),
sort: searchParams.get('sort') || 'relevance',
page: parseInt(searchParams.get('page')) || 1
};
const setFilter = (key, value) => {
const params = new URLSearchParams(searchParams);
// Reset page when changing filters
if (key !== 'page') {
params.delete('page');
}
if (Array.isArray(value)) {
params.delete(key);
value.forEach(v => params.append(key, v));
} else if (value) {
params.set(key, value);
} else {
params.delete(key);
}
router.push(`?${params.toString()}`);
};
const clearFilters = () => {
router.push(pathname);
};
return { filters, setFilter, clearFilters };
}This hook reads the current filter state from URL search params and provides functions to update individual filters or clear all of them. It automatically resets pagination when filters change.