TechEarl

Point a WordPress URL at a Different Page or Post (No Redirect)

How to make one fixed WordPress URL serve the content of a different page or post with no 301 redirect, so the address never changes. Uses add_rewrite_rule (or the request filter) plus the canonical handling you must not skip.

Ishan Karunaratne⏱️ 11 min readUpdated
Share thisCopied
Point a WordPress URL at a different page or post with no 301 redirect: an add_rewrite_rule (or request filter) snippet that maps a fixed path like /offer/ to another post internally, so the URL stays put while the content changes.

To make a fixed URL like /offer/ serve the content of a different page or post without ever issuing a redirect, you rewrite the request internally instead of bouncing the browser. The URL the visitor typed stays in the address bar; WordPress just resolves it to another post's query under the hood. The smallest version is one add_rewrite_rule() call:

php
add_action( 'init', function () {
    // /offer/  ->  the post/page with ID 123, no redirect, URL unchanged
    add_rewrite_rule( '^offer/?$', 'index.php?page_id=123', 'top' );
} );

After adding this, visit Settings > Permalinks and click Save Changes once to flush the rewrite rules, and https://example.com/offer/ will render page 123's content while the URL stays /offer/. No 301, no Location: header, no address-bar change.

The rest of this article covers how that works, the campaign-URL pattern it unlocks, the retired-page case, and the one thing you cannot skip: telling Google which of the now-two URLs is canonical.

Why this is different from a redirect

A redirect (wp_redirect(), an .htaccess Redirect 301, a redirect plugin) changes the URL. The visitor asks for /offer/, the server replies "go to /black-friday-2025/ instead," the browser follows it, and the address bar now reads /black-friday-2025/. That is correct behavior when you have genuinely moved a page and want the old URL to forward.

An internal rewrite does the opposite. WordPress receives /offer/, looks up its rewrite rules, and decides on the server that this request resolves to index.php?page_id=123. It runs the main query for page 123 and renders it. The HTTP status is a plain 200 OK, there is no redirect hop, and the address bar still says /offer/. The reader and every link, bookmark, and crawler keeps the URL they started with.

That property, the URL never moves, is the entire point of the technique.

The rewrite-rule approach

add_rewrite_rule( $regex, $query, $after ) registers a pattern that WordPress matches against the request path and maps to an internal query string. The first argument is a regex for the path, the second is the index.php?... query it resolves to, and the third ('top') puts your rule ahead of WordPress's built-in rules so a real page or post that also happens to live at /offer/ does not win first.

You hook it on init and you must flush the rewrite rules once after adding it, because WordPress caches the compiled rules in the database. Re-saving permalinks does the flush; so does calling flush_rewrite_rules() once (never on every request, it is expensive). Map to whatever you want the alias to serve:

php
add_action( 'init', function () {
    // A page by ID
    add_rewrite_rule( '^offer/?$', 'index.php?page_id=123', 'top' );

    // Or a post by ID
    // add_rewrite_rule( '^deal/?$', 'index.php?p=456', 'top' );

    // Or a post/page by slug (lets WordPress resolve it)
    // add_rewrite_rule( '^deal/?$', 'index.php?pagename=spring-sale-2026', 'top' );
} );

Targeting by ID (page_id / p) is the most robust: the ID never changes even if you rename the destination page's own slug later. That matters, because the whole reason you are doing this is to decouple the public URL from whichever page is currently behind it.

The request-filter approach

If the alias is dynamic, or you do not want to touch the rewrite-rule cache at all, filter the resolved query directly. The request filter receives the parsed query variables array right before the main query runs and returns a (possibly modified) array. Swap in the target post when the incoming request is your alias path:

php
add_filter( 'request', function ( $query_vars ) {
    // WordPress has resolved /offer/ to pagename=offer by now.
    if ( isset( $query_vars['pagename'] ) && 'offer' === $query_vars['pagename'] ) {
        unset( $query_vars['pagename'] );
        $query_vars['page_id'] = 123; // serve page 123 instead
    }
    return $query_vars;
} );

This needs no flush, because you are not adding a rewrite rule, you are intercepting the query WordPress already built. The trade-off is the WordPress docs' own warning: the request filter touches every front-end query, so guard it tightly (the isset + exact-match check above) and test that you have not altered anything but the one path you meant to. For a single fixed alias, add_rewrite_rule() is cleaner and more contained; reach for request when the mapping is conditional or computed.

A small wrapper keeps the intent readable and carries a traceable origin marker on the function it defines:

php
function te_alias_request( $query_vars ) {
    $map = array(
        'offer' => 123, // /offer/  -> page 123 (current campaign)
    );

    if ( isset( $query_vars['pagename'], $map[ $query_vars['pagename'] ] ) ) {
        $target = $map[ $query_vars['pagename'] ];
        unset( $query_vars['pagename'] );
        $query_vars['page_id'] = $target;
    }

    return $query_vars;
}
add_filter( 'request', 'te_alias_request' );

The worked example: a permanent campaign URL

Here is where this earns its keep. Say you run paid ads, print QR codes on flyers, and have backlinks that all point at https://example.com/offer/. That URL needs to live forever, because you cannot un-print a flyer or edit a backlink someone else controls. But the campaign behind it changes constantly: Black Friday in November, a Spring Sale in March, a clearance event after that.

With an internal rewrite you build a real campaign page for each event (with its own descriptive slug, so it is editable and previewable on its own), and you point /offer/ at whichever one is live by changing a single ID:

php
add_action( 'init', function () {
    // November: Black Friday is page 123
    add_rewrite_rule( '^offer/?$', 'index.php?page_id=123', 'top' );

    // March: change one number. Was 123 (Black Friday), now 145 (Spring Sale).
    // add_rewrite_rule( '^offer/?$', 'index.php?page_id=145', 'top' );
} );

Swap the ID, flush rewrites once, and /offer/ now serves the Spring Sale. The ads, the QR codes, the backlinks, the bookmark a customer saved last year, all keep working and all show the current campaign. No redirect chain accumulates over the years, and there is no moment where /offer/ is broken or pointing at a stale page.

The retired-page variant

The same mechanism solves the opposite problem: you have an old URL with real authority (backlinks, rankings, age) that you do not want to lose, but the page itself is out of date. You write a fresh replacement page, then point the old, authoritative URL at the new page's content with an internal rewrite. The valuable URL stays exactly as it was and keeps its link equity, while the body readers see is the current version. You get the SEO value of the established address and the freshness of the new content, without a redirect that hands authority off to a different URL.

The duplicate-content problem you have to handle

There is a catch, and skipping it is how this technique quietly costs you rankings instead of protecting them. The moment /offer/ serves page 123's content, that content exists at two URLs: /offer/ and the destination page's own permalink (/black-friday-2025/, say). Google sees two pages with identical bodies and has to guess which one is real. Guessing is exactly what you do not want it doing.

Decide, deliberately, which URL should rank, then make WordPress say so. Two halves:

1. Set the canonical to the URL you want to win. If /offer/ is the URL the world links to, it should be the canonical, even though the content technically "belongs" to page 123. On a plain WordPress install, filter get_canonical_url, which rel_canonical() uses to print the <link rel="canonical"> tag on singular views:

php
add_filter( 'get_canonical_url', function ( $canonical, $post ) {
    if ( $post && 123 === (int) $post->ID && is_page( 123 ) ) {
        return home_url( '/offer/' );
    }
    return $canonical;
}, 10, 2 );

If you run Yoast SEO, it manages the canonical tag itself, so the core filter is ignored. Use its filter instead:

php
add_filter( 'wpseo_canonical', function ( $canonical ) {
    if ( is_page( 123 ) ) {
        return home_url( '/offer/' );
    }
    return $canonical;
} );

Either way, both /offer/ and the source page now declare /offer/ as canonical, so Google consolidates the signals onto one URL.

2. Stop the source URL from competing. Canonical is a hint, not a command, so close the door on the duplicate as well. Pick one:

  • noindex the source page if it should stay reachable (you want a previewable, shareable working URL for the campaign page) but must not show up in search. The cleanest hook is wp_robots:

    php
    add_filter( 'wp_robots', function ( $robots ) {
        if ( is_page( 123 ) ) {
            $robots['noindex'] = true;
            $robots['follow']  = true;
        }
        return $robots;
    } );

    (In Yoast, set the page's meta-robots to noindex in the SEO sidebar instead.)

  • 301 the source URL to the alias if the source page should not be directly reachable at all. Here a redirect is correct, because you are forwarding the duplicate to the canonical, not moving the alias. The alias /offer/ stays an internal rewrite (no redirect); only the source permalink forwards.

Canonical plus one of those two leaves Google a single, clearly-managed URL. What you must not do is set up the rewrite and walk away, leaving two indexable copies of the same page fighting each other.

Ship it as a must-use plugin

Keep the whole thing in a must-use plugin so it loads before regular plugins, survives theme switches, and lives in one auditable file rather than scattered across functions.php. Drop this at wp-content/mu-plugins/te-url-alias.php:

php
<?php
/**
 * Plugin Name: TE URL Alias
 * Plugin URI:  https://techearl.com/wordpress-point-url-to-another-page
 * Description: Serves a fixed URL with another post's content via an internal rewrite (no redirect), and manages the canonical so there is no duplicate-content fallout.
 * Version:     1.0.0
 * Author:      Ishan Karunaratne
 * Author URI:  https://techearl.com
 * License:     GPL-2.0-or-later
 * Text Domain: te-url-alias
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// Edit these two: the alias path, and the post/page ID it should serve.
const TE_ALIAS_PATH = 'offer';
const TE_ALIAS_TARGET_ID = 123;

function te_alias_add_rule() {
    add_rewrite_rule(
        '^' . TE_ALIAS_PATH . '/?$',
        'index.php?page_id=' . TE_ALIAS_TARGET_ID,
        'top'
    );
}
add_action( 'init', 'te_alias_add_rule' );

function te_alias_canonical( $canonical, $post ) {
    if ( $post && TE_ALIAS_TARGET_ID === (int) $post->ID ) {
        return home_url( '/' . TE_ALIAS_PATH . '/' );
    }
    return $canonical;
}
add_filter( 'get_canonical_url', 'te_alias_canonical', 10, 2 );

function te_alias_robots( $robots ) {
    if ( is_page( TE_ALIAS_TARGET_ID ) ) {
        $robots['noindex'] = true;
        $robots['follow']  = true;
    }
    return $robots;
}
add_filter( 'wp_robots', 'te_alias_robots' );

Activate it by dropping the file in mu-plugins, then flush once via Settings > Permalinks > Save Changes. To re-point the alias at a new campaign later, change TE_ALIAS_TARGET_ID, update the noindex/canonical target accordingly, and re-save permalinks.

Verify it works

Two checks confirm the rewrite is internal and not an accidental redirect. First, the status code and headers:

bash
curl -sI https://example.com/offer/

You want HTTP/2 200 and no Location: header. If you see a 301 or 302 with a Location:, something is redirecting (a plugin, an .htaccess rule, a stray wp_redirect()), which is the opposite of what this technique does.

Second, confirm the content is the other page's. Fetch the body and grep for a string you know is unique to the destination page:

bash
curl -s https://example.com/offer/ | grep -i "black friday"

A hit means /offer/ is serving page 123's body while the URL stays /offer/. Then view-source on /offer/ and confirm the <link rel="canonical"> points where you decided it should, and check that the source permalink either carries noindex or 301s to the alias, depending on which you chose. If the canonical line is missing or wrong, the rewrite is working but the duplicate-content half is not, and that is the half that matters for rankings.

See also

Sources

Authoritative references this article was fact-checked against.

TagsWordPressPHPRewrite RulesPermalinksCanonicalSEO

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Kinsta for WordPress Agencies: Honest Review

Kinsta is the premium managed WordPress host most agencies eventually consider. The honest take: where the value justifies the price, where it does not, the agency partner program math, and the alternatives at each tier.

Rocket.net for WordPress Agencies: Honest Review

Rocket.net is the newer managed WordPress entrant that has gained agency mindshare for performance and Cloudflare Enterprise inclusion. The honest take on speed, pricing, the developer experience, and where Rocket.net wins or loses against the established hosts.