WordPress provides a nice little function for displaying the title of the current post: the_title(). This function gets used all over the place: in the site header, at the top of single posts and pages, in the loop, in the footer, etc. It is probably one of the most commonly used functions by theme developers, and sometimes plugin developers, depending on the plugin. What many developers don’t realize, however, is that there is actually a time that this function should not be used for retrieving and showing a post title, and that is in attributes.



I run into snippets like this a lot:

1 <a href=" <?php the_permalink ( ) ; ?> " title=" <?php the_title ( ) ; ?> ">Permalink to <?php the_title ( ) ; ?> </a> <a href="<?php the_permalink(); ?>" title="<?php the_title(); ?>">Permalink to <?php the_title(); ?></a>

In general this is fine and displays perfectly fine most of the time. The problem is simple: when outputting the title of a post in an attribute, you should always use the_title_attribute(). The reason comes down to escaping.

Let’s look at the source for the two functions.

the_title()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /** * Display or retrieve the current post title with optional content. * * @since 0.71 * * @param string $before Optional. Content to prepend to the title. * @param string $after Optional. Content to append to the title. * @param bool $echo Optional, default to true.Whether to display or return. * @return null|string Null on no title. String if $echo parameter is false. */ function the_title ( $before = '' , $after = '' , $echo = true ) { $title = get_the_title ( ) ; if ( strlen ( $title ) == 0 ) return ; $title = $before . $title . $after ; if ( $echo ) echo $title ; else return $title ; } /** * Display or retrieve the current post title with optional content. * * @since 0.71 * * @param string $before Optional. Content to prepend to the title. * @param string $after Optional. Content to append to the title. * @param bool $echo Optional, default to true.Whether to display or return. * @return null|string Null on no title. String if $echo parameter is false. */ function the_title($before = '', $after = '', $echo = true) { $title = get_the_title(); if ( strlen($title) == 0 ) return; $title = $before . $title . $after; if ( $echo ) echo $title; else return $title; }

This one doesn’t show us what we need, since it just calls get_the_title(), so let’s also look at the source for that function:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * Retrieve post title. * * If the post is protected and the visitor is not an admin, then "Protected" * will be displayed before the post title. If the post is private, then * "Private" will be located before the post title. * * @since 0.71 * * @param mixed $post Optional. Post ID or object. * @return string */ function get_the_title ( $post = 0 ) { $post = get_post ( $post ) ; $title = isset ( $post -> post_title ) ? $post -> post_title : '' ; $id = isset ( $post -> ID ) ? $post -> ID : 0 ; if ( ! is_admin ( ) ) { if ( ! empty ( $post -> post_password ) ) { $protected_title_format = apply_filters ( 'protected_title_format' , __ ( 'Protected: %s' ) ) ; $title = sprintf ( $protected_title_format , $title ) ; } else if ( isset ( $post -> post_status ) && 'private' == $post -> post_status ) { $private_title_format = apply_filters ( 'private_title_format' , __ ( 'Private: %s' ) ) ; $title = sprintf ( $private_title_format , $title ) ; } } return apply_filters ( 'the_title' , $title , $id ) ; } /** * Retrieve post title. * * If the post is protected and the visitor is not an admin, then "Protected" * will be displayed before the post title. If the post is private, then * "Private" will be located before the post title. * * @since 0.71 * * @param mixed $post Optional. Post ID or object. * @return string */ function get_the_title( $post = 0 ) { $post = get_post( $post ); $title = isset( $post->post_title ) ? $post->post_title : ''; $id = isset( $post->ID ) ? $post->ID : 0; if ( ! is_admin() ) { if ( ! empty( $post->post_password ) ) { $protected_title_format = apply_filters( 'protected_title_format', __( 'Protected: %s' ) ); $title = sprintf( $protected_title_format, $title ); } else if ( isset( $post->post_status ) && 'private' == $post->post_status ) { $private_title_format = apply_filters( 'private_title_format', __( 'Private: %s' ) ); $title = sprintf( $private_title_format, $title ); } } return apply_filters( 'the_title', $title, $id ); }

This function is pretty simple, it retrieves the post object using get_post() and then, after passing it through a filter called the_title, returns $post->post_title.

The important part of this function (to properly illustrate this issue) is the applied filter: apply_filters( ‘the_title’, $title, $id );

This filter can be used by developers to modify the output of the title, perhaps to add extra HTML markup.

the_title_attribute()

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * Sanitize the current title when retrieving or displaying. * * Works like {@link the_title()}, except the parameters can be in a string or * an array. See the function for what can be override in the $args parameter. * * The title before it is displayed will have the tags stripped and {@link * esc_attr()} before it is passed to the user or displayed. The default * as with {@link the_title()}, is to display the title. * * @since 2.3.0 * * @param string|array $args Optional. Override the defaults. * @return string|null Null on failure or display. String when echo is false. */ function the_title_attribute ( $args = '' ) { $title = get_the_title ( ) ; if ( strlen ( $title ) == 0 ) return ; $defaults = array ( 'before' => '' , 'after' => '' , 'echo' => true ) ; $r = wp_parse_args ( $args , $defaults ) ; extract ( $r , EXTR_SKIP ) ; $title = $before . $title . $after ; $title = esc_attr ( strip_tags ( $title ) ) ; if ( $echo ) echo $title ; else return $title ; } /** * Sanitize the current title when retrieving or displaying. * * Works like {@link the_title()}, except the parameters can be in a string or * an array. See the function for what can be override in the $args parameter. * * The title before it is displayed will have the tags stripped and {@link * esc_attr()} before it is passed to the user or displayed. The default * as with {@link the_title()}, is to display the title. * * @since 2.3.0 * * @param string|array $args Optional. Override the defaults. * @return string|null Null on failure or display. String when echo is false. */ function the_title_attribute( $args = '' ) { $title = get_the_title(); if ( strlen($title) == 0 ) return; $defaults = array('before' => '', 'after' => '', 'echo' => true); $r = wp_parse_args($args, $defaults); extract( $r, EXTR_SKIP ); $title = $before . $title . $after; $title = esc_attr(strip_tags($title)); if ( $echo ) echo $title; else return $title; }

This function also uses get_the_title() to retrieve the title of the post, but the final data that is returned is different from the_title(), primarily in that it is escaped. What does this mean? It means that it is safe to use inside of element attributes. It also strips all tags.

Let’s look at an example.

Assume your $post->post_title is this:

< span class = "title" > This is a title with span tags < / span > <span class="title">This is a title with span tags</span>

When outputted with the_title(), this will remain completely unchanged and will display as:

< span class = "title" > This is a title with span tags < / span > <span class="title">This is a title with span tags</span>

But when you output this title through the_title_attribute(), you get this:

This is a title with span tags This is a title with span tags

Notice that the span tags have been removed.

What if your title had quotation marks in it? Such as:

This is a title with "quotation" marks This is a title with "quotation" marks

With the_title(), the title will be outputted as:

This is a title with "quotation" marks This is a title with "quotation" marks

With the_title_attribute(), the title will be outputted as:

This is a title with " quotation " marks This is a title with "quotation" marks

Notice how the quotation marks have been converted to entities?. This is called escaping and it ensures that attributes, such as title=”the title here”, don’t end up getting closed too early.

If we use the_title() inside of an attribute and the title has quotation marks, we will end up with broken markup.

<span title=" <?php the_title ( ) ; ?> "> <?php the_title ( ) ; ?> </span> <span title="<?php the_title(); ?>"><?php the_title(); ?></span>

Results in:

< span title = "This is a title with " quotation " marks" > This is a title with "quotation" marks < / span > <span title="This is a title with "quotation" marks">This is a title with "quotation" marks</span>

Notice that the title attribute gets closed with the first ” around the word quotation. This results in completely broken markup that might look something like this:

In order to not break the markup, you should always use the_title_attribute() when showing the title of a post inside of an attribute. It’s a very appropriately named function.

<span title=" <?php the_title_attribute ( ) ; ?> "> <?php the_title ( ) ; ?> </span> <span title="<?php the_title_attribute(); ?>"><?php the_title(); ?></span>

The usage of the_title() in attribute tags has actually caused my huge headaches with Easy Digital Downloads. We use the the_title filter to add Schema.org micro data to products, which is excellent because the data is used by search engines to enhance search result entries with product data (price, rating, etc). The problem is that we get at least one support ticket every week from a user that has broken HTML markup (like that pictured above). The problem is prevalent enough that we are being forced to add an option to disable our schema.org micro data.

If you care about better compatibility with plugins, and simply doing the right thing, you should update any theme or plugin that uses the_title() incorrectly. Note, WordPress core itself has a similar problem as well, so it’s not just themes and plugins.