Canonical URL Strategy in WordPress: Avoiding Duplicate Content

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.

Leave Comment

Your email address will not be published. Required fields are marked *