Dynamically Overwrite WordPress Permalinks with Filters

Reading Time: 4 minutes

If you’re looking to overwrite the permalink for a post or page, WordPress provides you with a few functions to filter the permalink. post_link, page_link, and post_type_link.

These filters let you completely overwrite the URL or add query parameters to the end.

But why?

What led me down this path was the need to give a client a news post type that could sometimes link through to a detail page, but other times link directly to a third party.

Throw in a custom field for an external URL and a few filters later we’re all good.

A more common use case may be adding a parameter to the end of a URL to do some basic tracking. You could probably even get fancy and do some basic A/B testing.

Neat! How do I do It?

Let’s say we have a post type being created for sharing news stories. Some will link internally for more details, others will link externally directly to the source.

We’ll need to do a couple things. First, we need to give the user a meta field for entering the custom URL. I’d suggest using Advanced Custom Fields to take advantage of their URL specific field, but you can create a custom field yourself as well.

If you’d like to create your own check out this guide on adding your own custom metabox.

Let’s say we have a meta field called custom_link that can be set on the News post type and when that is set we want the the_permalink function to output the value of custom_link.

We need to filter the permalink on the news post type using the post_type_link filter.

function prefix_filter_news_permalink( $url, $post ) {
    // If the custom_link ACF field is set get it's value
    $custom_link = get_field( 'custom_link', $post->ID );
    
    // If the custom_link is set and the post type is news change the URL to the custom_link value
    if ( $custom_link && 'news' === get_post_type( $post->ID ) ) {
        $url = $custom_link;
    }

    // Return the value of the URL
    return $url;
}

add_filter( 'post_type_link', 'prefix_filter_news_permalink', 10, 2 );

Let’s break that code down.

First, we’re creating a function named prefix_filter_news_permalink that gets 2 parameters from the post_type_link filter. The $url which is the original permalink and $post which is the post object.

The function first sets $custom_link to the value of the custom_link ACF field. Then we do an if statement to check if the post type has the news slug.

If both of those conditions are true then we override the $url parameter to be the custom_link set for the post.

Then we return the $url everytime the function is called. Since we always want a link returned.

The last line add_filter( 'post_type_link', 'prefix_filter_news_permalink', 10, 2 ); adds our function to the post_type_link filter with priority 10 and passes 2 parameters. In this case $url and $post.

Now anywhere in our theme the_permalink or get_the_permalink is used for our News post type a custom link can be used instead.

Working Across Multiple Post Types

As I mentioned earlier there are 3 filters that can be used to filter links depending on what post type you’re targeting. post_link works for the Posts post type, page_link works for the Page post type, and post_type_link works for custom post types.

If you want to give the user the option to use a custom link on any post type you can get a little more creative. For example, I had the need to give the user the ability to use a custom link on any post type except attachments. Here’s the block of code I put together to do that then we’ll break it down.

/**
 * Rewrite the permalink for post types using the Custom Link option
 *
 * @param string $url  The original permalink.
 * @param object $post The post object.
 *
 * @since 1.0.0
 */
function prefix_custom_link_option( $url, $post ) {
    // Create an array of post types to skip.
    $skip_post_types   = array(
        'attachment',
    );

    // page_link gives the ID rather than the $post object.
    if ( 'integer' === gettype( $post ) ) {
        $post_id = $post;
    } else {
        $post_id = $post->ID;
    }

    // Check if the current post type should be skipped.
    if ( in_array( get_post_type( $post_id ), $skip_post_types, true ) ) {
        return $url;
    }

    // Get the custom_link if one exists.
    $custom_link = get_field( 'custom_link', $post_id );

    if ( $custom_link ) {
        $url = $custom_link['url'];
    }

    return $url;
}

/**
 * Add filters for post_link, page_link, and post_type_link to update Custom Link
 */
foreach ( [ 'post', 'page', 'post_type' ] as $post_type ) {
    add_filter( $post_type . '_link', 'prefix_custom_link_option', 10, 2 );
}

Our function is relatively similar, though at the top we’ve added an array of post types we want to skip. If the current post type is in the array we just return the $url unchanged.

Otherwise we continue the same as the previous function. Check for the custom_link field and if it’s set, use that as the $url.

The filter is a bit different. We’re actually running a foreach over an array of [ 'post', 'page', 'post_type' ]. Then adding a filter for each with '_link' appended. It’s a DRY (don’t repeat yourself) approach to writing out:

add_filter( 'post_link', 'prefix_custom_link_option', 10, 2 );
add_filter( 'page_link', 'prefix_custom_link_option', 10, 2 );
add_filter( 'post_type_link', 'prefix_custom_link_option', 10, 2 );

It works the same, but you only need to write the function name, priority, and number of params once.

Once caveat is that the page_link filter passes the post ID as an integer in the second parameter rather than the $post object. So I have a conditional check to the type of the $post variable to make sure it uses the actual ID.

Pin It on Pinterest