Read enough WordPress theme or plugin code and you’ll notice that values are almost never echoed directly. Instead you see echo esc_html( $title ); or <a href="<?php echo esc_url( $url ); ?>">. These are WordPress’s escaping functions, the core tools for preventing XSS (cross-site scripting). This post looks at why there are several of them and, more importantly, when escaping should happen.
Note: XSS is a broad term for problems where a value shown on a page contains text that the browser interprets as HTML or script rather than as plain text. The root cause is a blurred boundary between “a value” and “the structure of the HTML.” It’s the same underlying idea as
$wpdb->prepare()in the previous post, which keeps values separate from the structure of a SQL statement.
Why Escaping Is Needed
Take a search results page that displays “Search keyword: …” The keyword comes from the URL, so it’s an externally supplied value. Printing it as-is is where trouble starts.
// Not recommended (illustrative pseudo-code): outputs an external value directly
echo '<p>Search keyword: ' . $_GET['s'] . '</p>';
If $_GET['s'] contains characters like < or >, the browser reads them as HTML tags, not as text. What was meant to be a value becomes part of the page’s structure.
Escaping replaces characters that have special meaning in HTML with alternative notations (HTML entities), so the result is treated purely as displayed text. For example, < becomes < and > becomes >. On screen you still see a <, but the browser no longer treats it as a tag.
Why There Are Different Functions for Different Places
WordPress provides several escaping functions because which characters are “special” depends on where the value is placed.
| Function | Typical destination | What it guards |
|---|---|---|
esc_html() |
Text between HTML tags | Converts <, >, &, etc. to entities |
esc_attr() |
HTML attribute values (class="...") |
Keeps the value from breaking out of the attribute, including quotes |
esc_url() |
href / src and other URLs |
Removes unsuitable schemes and tidies invalid characters |
esc_js() |
Strings inside inline JavaScript | Handles characters that would break a string literal |
The same string can sit between tags, inside an attribute, or in a URL, and the browser’s idea of a “boundary” differs in each case: quotes are the boundary in an attribute, while the scheme (https:, etc.) matters in a URL. That’s why choosing the function that matches the destination is the key point. Putting an esc_html()-wrapped value into an attribute, or using esc_attr() output as a URL, leaves the protection misaligned.
Escape on Output, Not on Input
The second important idea is that escaping happens right before output, not when a value is saved. WordPress’s coding guidance sums this up as “escape late.”
The reason is that one stored value may be printed in several places: as text in one spot, as an attribute value in another, as part of a URL elsewhere. If you transform it for HTML at save time, it may get double-converted in one place or under-protected in another. Keeping the value in its original form and converting it at the moment of output, for that specific destination, makes it harder to lose track of the boundary.
Input cleanup such as sanitize_text_field() at save time has a different purpose, and neither replaces the other. Sanitizing makes a value acceptable as incoming data; escaping keeps it from being misread at its destination.
When You Want to Keep Some HTML
Sometimes you don’t want every tag treated as text. An archive page description entered in the admin, with paragraphs or emphasis, is one example. For that, WordPress has wp_kses_post(), which keeps only tags on an allow list and strips everything else. esc_html() (“escape everything”) and wp_kses_post() (“let only approved markup through”) are a choice based on how much HTML you want the output to carry.
How the wpmm-blog Theme Uses Them
Checking the files of this blog’s own theme, wpmm-blog, the escaping functions appear as follows (counts as of September 2026):
| Function | Occurrences in the theme | Example use |
|---|---|---|
esc_html() |
about 70 | Page titles, headings, text between tags |
esc_url() |
about 47 | Favicons, images, link targets |
esc_attr() |
about 29 | Attribute values (such as an analytics ID) |
esc_js() |
1 | A string inside an inline script |
wp_kses_post() |
1 | Archive description (some HTML allowed) |
The search results heading is an easy example. In index.php, the search keyword is printed as esc_html( get_search_query() ), so an externally supplied value is wrapped right at the point of output. Unlike the bad example above, a keyword containing < shows up on screen as plain text.
The JSON-LD structured data is output with wp_json_encode() rather than an HTML escaper. It’s a different destination, JSON, so a different function is chosen. The “match the destination” principle shows up here too.
A Quick Order of Questions
When unsure which function to use, work through these in order:
| Question | What to pick |
|---|---|
| Where in the HTML will the value be placed? | The function for that destination (text → esc_html, attribute → esc_attr, URL → esc_url) |
| Should part of the HTML be kept? | An allow-list function such as wp_kses_post() |
| When should the conversion happen? | Right before output, not at save time |
Summary
Escaping functions keep the boundary between “a value” and “the structure of the HTML” clear at the moment of output. There are several because each destination draws that boundary differently, and escaping happens late because one value may be used in many places. When writing or reading theme or plugin code, get in the habit of checking, for each echo, where the value ends up and whether the matching function wraps it, whether or not the value seems to come from outside. Read together with the previous $wpdb->prepare() post, it shows the same “don’t trust input” mindset taking different forms for two destinations: SQL and HTML.