Canonical URL tags tell search engines which URL is the definitive version of a page, preventing duplicate content penalties when the same content is reachable through multiple paths. WordPress automatically outputs canonical tags for standard post types through wp_head(), but it omits them for custom post types, custom taxonomies, and author archives by default. The SEO implications are significant on sites that use pretty permalinks, paginated archives, or query string parameters for sorting — each variation creates a potential duplicate. Hooking into wp_head with a priority lower than the default 10 lets your canonical tag appear before most other head meta output. WordPress conditional tags like is_singular(), is_category(), and is_post_type_archive() identify the current request type without manual URL parsing. Paginated archives increment the page number via the paged query variable, and the canonical should reflect the paginated URL rather than pointing all pages back to page one. For taxonomy archives the get_term_link() function returns a fully qualified canonical URL including any custom rewrite slug you have registered. Author archive canonicals must use get_author_posts_url() rather than get_the_author_meta('url'), which returns the author’s website, not their archive URL. Always wrap the URL with esc_url() before outputting it to prevent XSS via a crafted $_SERVER['REQUEST_URI'] value reaching the head. The structured data guide covers og:url and JSON-LD URL properties that should match the canonical for consistent signals. If you run WordPress in a subdirectory or behind a reverse proxy, verify that home_url() returns the correct origin before trusting the generated canonicals. The Nginx caching post is relevant here because cache keys should align with the canonical URL to avoid serving cached content under non-canonical paths. Disable this snippet immediately if you install Yoast SEO, Rank Math, or any other SEO plugin that outputs its own canonical tags to prevent duplicate link elements in the document head.
Problem: WordPress does not output rel="canonical" tags for custom post types, custom taxonomies, author archives, or paginated archive pages, creating duplicate content issues that dilute search rankings.
Solution: Hook into wp_head and use WordPress conditional tags to generate an accurate canonical URL for every page type, including paginated archives, and output it with esc_url().
add_action('wp_head', function() {
$canonical = '';
if (is_singular()) {
$canonical = get_permalink();
} elseif (is_category() || is_tag() || is_tax()) {
$term_link = get_term_link(get_queried_object());
$canonical = is_wp_error($term_link) ? '' : $term_link;
} elseif (is_author()) {
$canonical = get_author_posts_url(get_queried_object_id());
} elseif (is_post_type_archive()) {
$canonical = get_post_type_archive_link(get_post_type());
} elseif (is_home()) {
$for_posts = (int) get_option('page_for_posts');
$canonical = $for_posts ? get_permalink($for_posts) : home_url('/');
}
$paged = (int) get_query_var('paged');
if ($paged > 1 && !empty($canonical)) {
$canonical = trailingslashit($canonical) . 'page/' . $paged . '/';
}
if (!empty($canonical)) {
echo '<link rel="canonical" href="' . esc_url($canonical) . '">' . PHP_EOL;
}
}, 5);
NOTE: Use priority 5 so the canonical outputs before other plugins hook in at priority 10 — if two canonical tags are present, crawlers use whichever appears first in the source.