When using query strings to manage client-side state, it’s important to consider
the SEO implications. Search engines may index multiple variations of the same page
with different query parameters, leading to duplicate content issues.
Canonical URLs for local state
If your page uses query strings for local-only state (like UI preferences, filters,
or pagination), you should add a canonical URL to your page. This tells SEO crawlers
to ignore the query string and index the page without it.
Next.js App Router
In the Next.js app router, this is done via the metadata object:
import type { Metadata } from 'next'
export const metadata: Metadata = {
alternates: {
canonical: '/products' // URL path without query string
}
}
export default function ProductsPage() {
// Component using nuqs for filters, sorting, etc.
return <ProductList />
}
Next.js Pages Router
For the pages router, use the Head component:
import Head from 'next/head'
export default function ProductsPage() {
return (
<>
<Head>
<link rel="canonical" href="https://example.com/products" />
</Head>
<ProductList />
</>
)
}
Canonical URLs with meaningful query strings
If however the query string is defining what content the page is displaying
(like YouTube’s watch URLs: https://www.youtube.com/watch?v=dQw4w9WgXcQ),
your canonical URL should contain the relevant query strings.
You can use your parsers to read the search params and serialize the canonical URL:
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import {
createParser,
createLoader,
createSerializer,
type SearchParams,
type UrlKeys
} from 'nuqs/server'
const youTubeVideoIdRegex = /^[^"&?\/\s]{11}$/i
const youTubeSearchParams = {
videoId: createParser({
parse(query) {
if (!youTubeVideoIdRegex.test(query)) {
return null
}
return query
},
serialize(videoId) {
return videoId
}
})
}
const youTubeUrlKeys: UrlKeys<typeof youTubeSearchParams> = {
videoId: 'v'
}
const loadYouTubeSearchParams = createLoader(
youTubeSearchParams,
{ urlKeys: youTubeUrlKeys }
)
const serializeYouTubeSearchParams = createSerializer(
youTubeSearchParams,
{ urlKeys: youTubeUrlKeys }
)
type Props = {
searchParams: Promise<SearchParams>
}
export async function generateMetadata({
searchParams
}: Props): Promise<Metadata> {
const { videoId } = await loadYouTubeSearchParams(searchParams)
if (!videoId) {
notFound()
}
return {
alternates: {
canonical: serializeYouTubeSearchParams('/watch', { videoId })
// Result: /watch?v=dQw4w9WgXcQ
}
}
}
export default async function WatchPage({ searchParams }: Props) {
const { videoId } = await loadYouTubeSearchParams(searchParams)
return <VideoPlayer videoId={videoId} />
}
When to use canonical URLs
Use canonical URLs when:
- UI state: Filters, sorting, view modes, expanded sections
- Pagination: Page numbers that don’t change core content
- Client preferences: Theme, language (if not affecting content), layout options
- Temporary state: Modal open/close, selected tab
Don’t use canonical URLs (or include query params in canonical) when:
- Content identification: Unique resource IDs (like video IDs, article slugs)
- Search queries: User search terms that change results
- Deep linking: State that defines what the user should see
- Shareability: URLs meant to be bookmarked or shared
Multiple query parameters
When you have multiple query parameters, decide which ones are meaningful for SEO:
import type { Metadata } from 'next'
import { createLoader, createSerializer, parseAsString, parseAsInteger } from 'nuqs/server'
const searchParamsConfig = {
category: parseAsString, // Meaningful for SEO
sort: parseAsString, // UI preference
page: parseAsInteger, // Pagination
view: parseAsString // UI preference
}
const serializeSearchParams = createSerializer(searchParamsConfig)
export async function generateMetadata({ searchParams }) {
const { category } = await createLoader(searchParamsConfig)(searchParams)
return {
alternates: {
// Only include category in canonical URL
canonical: category
? serializeSearchParams('/products', { category })
: '/products'
}
}
}
robots.txt considerations
For query parameters that are purely for UI state, you can also use robots.txt
to tell search engines to ignore specific parameters:
User-agent: *
Allow: /
# Ignore specific query parameters
Disallow: /*?*sort=
Disallow: /*?*page=
Disallow: /*?*view=
Be careful with robots.txt rules. They affect all pages on your site and
can accidentally block important content if not configured correctly.
Open Graph and Twitter Cards
When sharing links on social media, you may want to remove query parameters
from the Open Graph URL:
import type { Metadata } from 'next'
export const metadata: Metadata = {
alternates: {
canonical: '/products'
},
openGraph: {
url: 'https://example.com/products' // Clean URL without query params
},
twitter: {
card: 'summary_large_image',
}
}
Or include them if they’re meaningful:
export async function generateMetadata({ searchParams }) {
const { category } = await loadSearchParams(searchParams)
const url = category
? `https://example.com/products?category=${category}`
: 'https://example.com/products'
return {
alternates: { canonical: url },
openGraph: { url }
}
}
Google Search Console
Monitor how Google indexes your pages with different query parameters:
- Check the URL Parameters section in Google Search Console
- Configure how Googlebot should treat specific parameters
- Use the URL Inspection tool to verify canonical tags are working
Best practices summary
- Always set a canonical URL when using query strings for UI state
- Include meaningful query params in canonical URLs for content identification
- Use loaders and serializers to keep canonical URL logic DRY
- Test your canonical tags using browser DevTools or SEO analysis tools
- Monitor in Search Console to ensure proper indexing
- Be consistent - use the same canonical URL strategy across your site