A wrong or missing canonical URL causes search engines to index duplicate content, splitting your link equity across multiple URLs. WordPress outputs a canonical by default, but pagination, query strings, and custom post types often need manual correction.
Problem: A WordPress site generates duplicate content across pagination, category/tag archives, attachment pages, and URL variants with trailing slashes or query strings — harming SEO without visible symptoms.
Solution: Audit every canonical source: enable rel="canonical" via wp_head, set noindex on attachment and date archives using the wp_robots filter, and configure 301 redirects in .htaccess or Nginx for www/non-www and HTTP/HTTPS normalisation. Verify with curl -I checks on common duplicate patterns.
The examples below override the WordPress canonical for custom post types, fix pagination canonicals, and add a self-referencing canonical to REST API responses.
// 1. Force canonical for a custom post type to always use the pretty permalink
add_filter( 'get_canonical_url', function( $canonical, $post ) {
if ( 'product' === $post->post_type ) {
$canonical = get_permalink( $post->ID );
}
return $canonical;
}, 10, 2 );
// 2. Remove query-string parameters from canonical
// e.g. ?utm_source=newsletter should not create a separate canonical
add_filter( 'get_canonical_url', function( $canonical ) {
$parsed = parse_url( $canonical );
// Rebuild URL without query string
return $parsed['scheme'] . '://' . $parsed['host'] . ( $parsed['path'] ?? '/' );
} );
// 3. Paginated archives: WordPress handles /?paged=2 → /page/2/ automatically
// but if you use a custom query, you must set the canonical yourself:
add_action( 'wp_head', function() {
if ( is_page() && get_query_var( 'paged' ) > 1 ) {
$canonical = get_pagenum_link( get_query_var( 'paged' ) );
echo '' . "
";
}
}, 1 ); // priority 1 — run before Yoast/RankMath output
Add canonical links to REST API responses for headless setups:
// Add canonical URL field to REST API post responses
add_action( 'rest_api_init', function() {
register_rest_field( 'post', 'canonical_url', [
'get_callback' => function( $post_arr ) {
return get_permalink( $post_arr['id'] );
},
'schema' => [
'description' => 'Canonical URL of the post',
'type' => 'string',
'format' => 'uri',
],
] );
} );
// Redirect non-canonical URLs to the canonical version (301)
add_action( 'template_redirect', function() {
if ( ! is_singular() ) return;
$canonical = get_permalink();
$requested = ( is_ssl() ? 'https' : 'http' ) . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
// Strip trailing slash differences before comparing
if ( untrailingslashit( $canonical ) !== untrailingslashit( strtok( $requested, '?' ) ) ) {
wp_redirect( $canonical, 301 );
exit;
}
} );
NOTE: Always test canonicals with curl -I https://yoursite.com/your-post/?random_param=1 and confirm the Link: <…>; rel="canonical" header or the <link rel="canonical"> tag points to the clean URL.