* WordPress Link Template Functions
* Displays the permalink for the current post.
* @since 4.4.0 Added the `$post` parameter.
* @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`.
function the_permalink( $post = 0 ) {
* Filters the display of the permalink for the current post.
* @since 4.4.0 Added the `$post` parameter.
* @param string $permalink The permalink for the current post.
* @param int|WP_Post $post Post ID, WP_Post object, or 0. Default 0.
echo esc_url( apply_filters( 'the_permalink', get_permalink( $post ), $post ) );
* Retrieves a trailing-slashed string if the site is set for adding trailing slashes.
* Conditionally adds a trailing slash if the permalink structure has a trailing
* slash, strips the trailing slash if not. The string is passed through the
* {@see 'user_trailingslashit'} filter. Will remove trailing slash from string, if
* site is not set to have them.
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
* @param string $string URL with or without a trailing slash.
* @param string $type_of_url Optional. The type of URL being considered (e.g. single, category, etc)
* for use in the filter. Default empty string.
* @return string The URL with the trailing slash appended or stripped.
function user_trailingslashit( $string, $type_of_url = '' ) {
if ( $wp_rewrite->use_trailing_slashes ) {
$string = trailingslashit( $string );
$string = untrailingslashit( $string );
* Filters the trailing-slashed string, depending on whether the site is set to use trailing slashes.
* @param string $string URL with or without a trailing slash.
* @param string $type_of_url The type of URL being considered. Accepts 'single', 'single_trackback',
* 'single_feed', 'single_paged', 'commentpaged', 'paged', 'home', 'feed',
* 'category', 'page', 'year', 'month', 'day', 'post_type_archive'.
return apply_filters( 'user_trailingslashit', $string, $type_of_url );
* Displays the permalink anchor for the current post.
* The permalink mode title will use the post title for the 'a' element 'id'
* attribute. The id mode uses 'post-' with the post ID for the 'id' attribute.
* @param string $mode Optional. Permalink mode. Accepts 'title' or 'id'. Default 'id'.
function permalink_anchor( $mode = 'id' ) {
switch ( strtolower( $mode ) ) {
$title = sanitize_title( $post->post_title ) . '-' . $post->ID;
echo '<a id="' . $title . '"></a>';
echo '<a id="post-' . $post->ID . '"></a>';
* Determine whether post should always use a plain permalink structure.
* @param WP_Post|int|null $post Optional. Post ID or post object. Defaults to global $post.
* @param bool|null $sample Optional. Whether to force consideration based on sample links.
* If omitted, a sample link is generated if a post object is passed
* with the filter property set to 'sample'.
* @return bool Whether to use a plain permalink structure.
function wp_force_plain_post_permalink( $post = null, $sample = null ) {
isset( $post->filter ) &&
'sample' === $post->filter
$post = get_post( $post );
$sample = null !== $sample ? $sample : false;
$post_status_obj = get_post_status_object( get_post_status( $post ) );
$post_type_obj = get_post_type_object( get_post_type( $post ) );
if ( ! $post_status_obj || ! $post_type_obj ) {
// Publicly viewable links never have plain permalinks.
is_post_status_viewable( $post_status_obj ) ||
// Private posts don't have plain permalinks if the user can read them.
$post_status_obj->private &&
current_user_can( 'read_post', $post->ID )
// Protected posts don't have plain links if getting a sample URL.
( $post_status_obj->protected && $sample )
* Retrieves the full permalink for the current post or post ID.
* This function is an alias for get_permalink().
* @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`.
* @param bool $leavename Optional. Whether to keep post name or page name. Default false.
* @return string|false The permalink URL or false if post does not exist.
function get_the_permalink( $post = 0, $leavename = false ) {
return get_permalink( $post, $leavename );
* Retrieves the full permalink for the current post or post ID.
* @param int|WP_Post $post Optional. Post ID or post object. Default is the global `$post`.
* @param bool $leavename Optional. Whether to keep post name or page name. Default false.
* @return string|false The permalink URL or false if post does not exist.
function get_permalink( $post = 0, $leavename = false ) {
$leavename ? '' : '%postname%',
$leavename ? '' : '%pagename%',
if ( is_object( $post ) && isset( $post->filter ) && 'sample' === $post->filter ) {
$post = get_post( $post );
if ( empty( $post->ID ) ) {
if ( 'page' === $post->post_type ) {
return get_page_link( $post, $leavename, $sample );
} elseif ( 'attachment' === $post->post_type ) {
return get_attachment_link( $post, $leavename );
} elseif ( in_array( $post->post_type, get_post_types( array( '_builtin' => false ) ), true ) ) {
return get_post_permalink( $post, $leavename, $sample );
$permalink = get_option( 'permalink_structure' );
* Filters the permalink structure for a post before token replacement occurs.
* Only applies to posts with post_type of 'post'.
* @param string $permalink The site's permalink structure.
* @param WP_Post $post The post in question.
* @param bool $leavename Whether to keep the post name.
$permalink = apply_filters( 'pre_post_link', $permalink, $post, $leavename );
! wp_force_plain_post_permalink( $post )
if ( strpos( $permalink, '%category%' ) !== false ) {
$cats = get_the_category( $post->ID );
* Filters the category that gets used in the %category% permalink token.
* @param WP_Term $cat The category to use in the permalink.
* @param array $cats Array of all categories (WP_Term objects) associated with the post.
* @param WP_Post $post The post in question.
$category_object = apply_filters( 'post_link_category', $cats[0], $cats, $post );
$category_object = get_term( $category_object, 'category' );
$category = $category_object->slug;
if ( $category_object->parent ) {
$category = get_category_parents( $category_object->parent, false, '/', true ) . $category;
// Show default category in permalinks,
// without having to assign it explicitly.
if ( empty( $category ) ) {
$default_category = get_term( get_option( 'default_category' ), 'category' );
if ( $default_category && ! is_wp_error( $default_category ) ) {
$category = $default_category->slug;
if ( strpos( $permalink, '%author%' ) !== false ) {
$authordata = get_userdata( $post->post_author );
$author = $authordata->user_nicename;
// This is not an API call because the permalink is based on the stored post_date value,
// which should be parsed as local time regardless of the default PHP timezone.
$date = explode( ' ', str_replace( array( '-', ':' ), ' ', $post->post_date ) );
$permalink = home_url( str_replace( $rewritecode, $rewritereplace, $permalink ) );
$permalink = user_trailingslashit( $permalink, 'single' );
} else { // If they're not using the fancy permalink option.
$permalink = home_url( '?p=' . $post->ID );
* Filters the permalink for a post.
* Only applies to posts with post_type of 'post'.
* @param string $permalink The post's permalink.
* @param WP_Post $post The post in question.
* @param bool $leavename Whether to keep the post name.
return apply_filters( 'post_link', $permalink, $post, $leavename );
* Retrieves the permalink for a post of a custom post type.
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
* @param int|WP_Post $id Optional. Post ID or post object. Default is the global `$post`.
* @param bool $leavename Optional. Whether to keep post name. Default false.
* @param bool $sample Optional. Is it a sample permalink. Default false.
* @return string|WP_Error The post permalink.
function get_post_permalink( $id = 0, $leavename = false, $sample = false ) {
if ( is_wp_error( $post ) ) {
$post_link = $wp_rewrite->get_extra_permastruct( $post->post_type );
$slug = $post->post_name;
$force_plain_link = wp_force_plain_post_permalink( $post );
$post_type = get_post_type_object( $post->post_type );
if ( $post_type->hierarchical ) {
$slug = get_page_uri( $post );
if ( ! empty( $post_link ) && ( ! $force_plain_link || $sample ) ) {
$post_link = str_replace( "%$post->post_type%", $slug, $post_link );
$post_link = home_url( user_trailingslashit( $post_link ) );
if ( $post_type->query_var && ( isset( $post->post_status ) && ! $force_plain_link ) ) {
$post_link = add_query_arg( $post_type->query_var, $slug, '' );
$post_link = add_query_arg(
'post_type' => $post->post_type,
$post_link = home_url( $post_link );
* Filters the permalink for a post of a custom post type.
* @param string $post_link The post's permalink.
* @param WP_Post $post The post in question.
* @param bool $leavename Whether to keep the post name.
* @param bool $sample Is it a sample permalink.
return apply_filters( 'post_type_link', $post_link, $post, $leavename, $sample );
* Retrieves the permalink for the current page or page ID.
* Respects page_on_front. Use this one.
* @param int|WP_Post $post Optional. Post ID or object. Default uses the global `$post`.
* @param bool $leavename Optional. Whether to keep the page name. Default false.
* @param bool $sample Optional. Whether it should be treated as a sample permalink.
* @return string The page permalink.
function get_page_link( $post = false, $leavename = false, $sample = false ) {
$post = get_post( $post );
if ( 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) == $post->ID ) {
$link = _get_page_link( $post, $leavename, $sample );
* Filters the permalink for a page.
* @param string $link The page's permalink.
* @param int $post_id The ID of the page.
* @param bool $sample Is it a sample permalink.
return apply_filters( 'page_link', $link, $post->ID, $sample );
* Retrieves the page permalink.
* Ignores page_on_front. Internal use only.
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
* @param int|WP_Post $post Optional. Post ID or object. Default uses the global `$post`.
* @param bool $leavename Optional. Whether to keep the page name. Default false.
* @param bool $sample Optional. Whether it should be treated as a sample permalink.
* @return string The page permalink.
function _get_page_link( $post = false, $leavename = false, $sample = false ) {
$post = get_post( $post );
$force_plain_link = wp_force_plain_post_permalink( $post );
$link = $wp_rewrite->get_page_permastruct();
if ( ! empty( $link ) && ( ( isset( $post->post_status ) && ! $force_plain_link ) || $sample ) ) {
$link = str_replace( '%pagename%', get_page_uri( $post ), $link );
$link = home_url( $link );
$link = user_trailingslashit( $link, 'page' );
$link = home_url( '?page_id=' . $post->ID );
* Filters the permalink for a non-page_on_front page.
* @param string $link The page's permalink.
* @param int $post_id The ID of the page.
return apply_filters( '_get_page_link', $link, $post->ID );
* Retrieves the permalink for an attachment.
* This can be used in the WordPress Loop or outside of it.
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
* @param int|object $post Optional. Post ID or object. Default uses the global `$post`.
* @param bool $leavename Optional. Whether to keep the page name. Default false.
* @return string The attachment permalink.
function get_attachment_link( $post = null, $leavename = false ) {
$post = get_post( $post );
$force_plain_link = wp_force_plain_post_permalink( $post );
$parent_id = $post->post_parent;
$parent = $parent_id ? get_post( $parent_id ) : false;
$parent_valid = true; // Default for no parent.
$post->post_parent === $post->ID ||
! is_post_type_viewable( get_post_type( $parent ) )
// Post is either its own parent or parent post unavailable.
if ( $force_plain_link || ! $parent_valid ) {
} elseif ( $wp_rewrite->using_permalinks() && $parent ) {
if ( 'page' === $parent->post_type ) {
$parentlink = _get_page_link( $post->post_parent ); // Ignores page_on_front.
$parentlink = get_permalink( $post->post_parent );
if ( is_numeric( $post->post_name ) || false !== strpos( get_option( 'permalink_structure' ), '%category%' ) ) {
$name = 'attachment/' . $post->post_name; // <permalink>/<int>/ is paged so we use the explicit attachment marker.