nuqs

SEO

Pitfalls and best practices for SEO with query strings

If your page uses query strings for local-only state, you should add a canonical URL to your page, to tell SEO crawlers to ignore the query string and index the page without it.

In the Next.js app router, this is done via the metadata object:

import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  alternates: {
    canonical: '/url/path/without/querystring'
  }
}

If however the query string is defining what content the page is displaying (eg: YouTube’s watch URLs, like https://www.youtube.com/watch?v=dQw4w9WgXcQ), your canonical URL should contain relevant query strings, and you can still use your parsers to read it, and to serialize the canonical URL.

/app/watch/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
import { notFound } from "next/navigation";
import {
  createParser,
  parseAsString,
  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 })
      // /watch?v=dQw4w9WgXcQ
    }
  }
}