Escaping Output
Escape at the point of output, every time, with the function that matches the context: esc_html, esc_attr, esc_url, esc_js or wp_kses.
esc_html( string $text ): stringEscaping converts a value into something that cannot change the structure of the document it is printed into. WordPress's rule is escape late — at the echo, not when the value was fetched — and escape every time, including values you sanitized yourself. Someone reading the template must be able to see the output is safe without tracing where the variable came from.
Match the function to the context
| Function | Context |
|---|---|
esc_html( $text ) | Text between tags. Converts specials with ENT_QUOTES and avoids double-encoding. |
esc_attr( $text ) | Inside a quoted attribute value. |
esc_url( $url, $protocols = null, $_context = 'display' ) | href, src, action. Returns an empty string if the protocol is not allowed. |
esc_textarea( $text ) | Contents of a <textarea>. |
esc_js( $text ) | Inline JS in an HTML event attribute, inside single quotes. |
wp_kses( $content, $allowed_html, $allowed_protocols = array() ) | When the HTML has to survive. |
Where it goes wrong
esc_html() on a URL in an href is not equivalent to esc_url() — esc_html() leaves javascript: perfectly intact. esc_js() is narrow: it is for a value inside an onclick="..." style attribute. For a value going into a <script> block, use wp_json_encode(), which supplies its own delimiting quotes.
Translations
esc_html__(), esc_html_e(), esc_attr__() and esc_attr_e() are shorthand for __() and _e() with the escape applied, so there is never a reason to print an unescaped translated string.
Example
<?php
/**
* single-acme_event.php — every value escaped at the point it is printed,
* with the function that matches that exact context.
*/
$venue = get_post_meta( get_the_ID(), '_acme_venue', true );
$website = get_post_meta( get_the_ID(), '_acme_website', true );
$capacity = (int) get_post_meta( get_the_ID(), '_acme_capacity', true );
$notes = get_post_meta( get_the_ID(), '_acme_notes', true );
<article post_class(); >
<h1> echo esc_html( get_the_title() ); </h1>
<!-- The same value twice: attribute context and text context differ. -->
<p data-venue=" echo esc_attr( $venue ); ">
echo esc_html( $venue );
— echo esc_html( number_format_i18n( $capacity ) ); seats
</p>
if ( $website ) :
<!-- esc_url(), not esc_html(): esc_html() would leave javascript: usable. -->
<a href=" echo esc_url( $website ); " rel="noopener">
// Shorthand:
esc_html_e( 'Event website', 'acme' );
// Longhand — identical:
// echo esc_html( __( 'Event website', 'acme' ) );
</a>
endif;
<!-- Notes hold real HTML, so filter rather than entity-encode. -->
<div class="event-notes"> echo wp_kses_post( $notes ); </div>
</article>When to use it
- Printing post meta or option values in a theme template without opening an XSS hole
- Building an admin list table where each cell holds user-supplied text
- Handing configuration from PHP to a front-end script safely
- Passing a plugin through the WordPress.org review, which flags every unescaped echo
More examples
Passing data to JavaScript
esc_js() and wp_json_encode() are not alternatives; they belong to different contexts. esc_js() escapes a value sitting inside a single-quoted JS string inside an HTML attribute, so the surrounding quotes are yours to write. wp_json_encode() produces a complete JS literal including its own quotes, which is what a <script> block needs. Using esc_js() in a script block leaves HTML entities in your string; using wp_json_encode() in an attribute leaves raw double quotes that close the attribute.
<?php
$label = get_option( 'acme_button_label' ); // arbitrary text
$config = array( 'endpoint' => rest_url( 'acme/v1/stats' ), 'label' => $label );
?>
<!-- Inline handler attribute: esc_js(), and the JS string is single-quoted. -->
<button onclick="acmeTrack('<?php echo esc_js( $label ); ?>')">
<?php echo esc_html( $label ); ?>
</button>
<!-- Script block: wp_json_encode() supplies the quotes and the escaping. -->
<script>
window.acmeConfig = <?php echo wp_json_encode( $config ); ?>;
</script>
<?php
// Better still for enqueued scripts — no inline output to escape at all.
wp_localize_script( 'acme-frontend', 'acmeConfig', $config );Escaping inside printf and attribute loops
printf() with numbered placeholders keeps a block of markup readable while still escaping each substitution separately, and it makes the string translatable as one unit. The second half shows the pattern for attributes built from an array: escape each key and value as it is written, rather than assembling a string and escaping it once at the end, which would destroy the quotes you just added.
<?php
foreach ( $events as $event ) {
printf(
'<li class="event"><a href="%1$s" title="%2$s">%3$s</a> <span>%4$s</span></li>',
esc_url( get_permalink( $event ) ),
esc_attr( get_the_title( $event ) ),
esc_html( get_the_title( $event ) ),
esc_html( get_the_date( '', $event ) )
);
}
// Building attributes from an array: escape per pair, as you write each one.
$atts = array(
'id' => 'acme-widget-' . $instance_id,
'data-mode' => $mode,
'aria-label' => __( 'Upcoming events', 'acme' ),
);
$html = '';
foreach ( $atts as $key => $value ) {
$html .= sprintf( ' %s="%s"', esc_attr( $key ), esc_attr( $value ) );
}
echo '<div' . $html . '></div>'; // $html is already fully escaped, pair by pairValues that must not be escaped as HTML
Some values are already markup and escaping them would print tags to the screen. wp_kses_post() is the right tool for stored HTML; the_content() runs its own filters and is output-ready. The last case is the one that catches people: a translated string containing a link cannot go through esc_html__(), so wrap the whole thing in wp_kses() with a tight allowlist instead of skipping escaping altogether.
<?php
// Already-filtered core output — do not wrap these in esc_html().
the_content();
the_excerpt();
// Stored HTML from an editor field: filter, do not entity-encode.
echo wp_kses_post( get_post_meta( get_the_ID(), '_acme_notes', true ) );
// A translated string containing markup. esc_html__() would print the <a> tags
// literally, so allow just the elements the translation is permitted to use.
$allowed = array( 'a' => array( 'href' => array(), 'rel' => array() ), 'strong' => array() );
echo wp_kses(
sprintf(
/* translators: %s: settings page URL */
__( 'No API key yet. <a href="%s">Add one in settings</a>.', 'acme' ),
esc_url( admin_url( 'options-general.php?page=acme' ) ) // still escape the substitution
),
$allowed
);
Discussion