Codeskill

Learn to code, step by step

HTML in WordPress/PHP templates (escaping, trust boundaries)

WordPress themes and PHP templates mix server-side logic with HTML output. The useful bit is how to write that HTML safely: what to escape, what to trust, and where XSS slips in when you assume the database is friendly.

You already know HTML structure. Here the HTML is often built from variables – post titles, user comments, custom fields – and echoed into the page. The tags are yours; the data usually is not.

Trust boundaries

Think about where data came from before you print it:

  • Trusted – hard-coded strings in your theme, written by you.
  • Semi-trusted – content from logged-in editors who can use HTML in the admin (still sanitise on save; escape on output unless you deliberately allow HTML).
  • Untrusted – comment text, search queries reflected on the page, anything from a visitor, data imported from elsewhere.

Default stance: escape on output. Allow HTML only where the platform already sanitises it (WordPress content filters) and you understand what tags survive.

WordPress escaping functions

WordPress provides helpers for common contexts. Use the one that matches where the value lands:

  • esc_html() – text inside HTML elements.
  • esc_attr() – attribute values.
  • esc_url() – URLs in href, src, and similar.
  • wp_kses_post() – allow a safe subset of HTML (typical post content).
  • wp_kses() – allow a custom list of tags and attributes.

Text content: esc_html

Post titles, author names, plain-text custom fields:

<?php
$title = get_the_title();
?>
<article>
  <h1><?php echo esc_html( $title ); ?></h1>
</article>

Without escaping, a title like <script>alert('x')</script> becomes executable markup if someone compromised the admin or imported bad data.

Attributes: esc_attr

Anything inside quotes on a tag:

<?php
$slug = get_post_field( 'post_name', get_the_ID() );
?>
<section id="<?php echo esc_attr( 'section-' . $slug ); ?>" class="content-section">
  <!-- ... -->
</section>

Attribute injection is easy to miss. A value containing a double quote can break out of the attribute and add event handlers if you do not escape.

URLs: esc_url

<?php
$website = get_user_meta( get_the_author_meta( 'ID' ), 'website', true );
if ( $website ) :
?>
  <p><a href="<?php echo esc_url( $website ); ?>">Author website</a></p>
<?php endif; ?>

esc_url rejects dangerous schemes like javascript:. Still verify the URL makes sense for your feature; escaping is not the same as validation.

Allowing HTML: wp_kses

When editors need basic formatting in a custom field:

<?php
$bio = get_post_meta( get_the_ID(), 'team_bio', true );
$allowed = array(
  'p'      => array(),
  'a'      => array(
    'href'   => array(),
    'title'  => array(),
  ),
  'strong' => array(),
  'em'     => array(),
);
?>
<div class="team-bio">
  <?php echo wp_kses( $bio, $allowed ); ?>
</div>

Post content from the block editor typically uses the_content(), which runs WordPress filters. Do not double-escape filtered content. Do escape anything you print outside those filters.

Template parts and reusable HTML

WordPress themes split markup into partial files:

<?php
// In header.php or front-page.php
get_template_part( 'template-parts/header/site', 'header' );
?>

<main id="main">
  <?php
  if ( have_posts() ) :
    while ( have_posts() ) :
      the_post();
      get_template_part( 'template-parts/content', get_post_type() );
    endwhile;
  endif;
  ?>
</main>

<?php get_template_part( 'template-parts/footer/site', 'footer' ); ?>

Keep static HTML structure in template parts; keep escaping next to every dynamic echo. A partial reused in six places is six places that need the same discipline.

Plain PHP (non-WordPress) escaping

Outside WordPress, PHP offers htmlspecialchars() with ENT_QUOTES and UTF-8:

<?php
function e( string $value ): void {
  echo htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
}

$name = $_GET['name'] ?? '';
?>
<p>Hello, <?php e( $name ); ?>.</p>

Many frameworks wrap this in template helpers. The rule is unchanged: escape for the context (HTML body, attribute, JavaScript, URL).

Common mistakes

  • Using esc_html() on HTML you intend to render as markup (you get visible &lt;p&gt; on screen).
  • Printing the_field() or custom meta without knowing whether it contains HTML.
  • Building HTML strings in PHP with unescaped concatenation, then echoing the blob.
  • Trusting commenter-supplied HTML because “they seem fine”.
  • Using <?php echo $var; ?> with no escaping because it is “only admin”.

HTML you control vs HTML from the CMS

Theme markup – landmarks, nav, wrappers – should be clean, semantic, and static in your template files. CMS content – paragraphs, embeds, blocks – flows through WordPress filters. Know which layer you are editing. Mixing huge chunks of layout HTML into post bodies makes auditing and escaping harder for everyone who follows you.

Escape by default. Allow HTML deliberately. Test with awkward input: quotes, angle brackets, javascript: URLs, and emoji. If the page still behaves, you are most of the way there.

PreviousAuditing markup (validators, outlines, smell tests)