Everything was working fine… until I wrapped my date with a <time> tag.

No errors. No warnings. The markup simply disappeared.

If you've ever used wp_kses_post() in WordPress and wondered why semantic HTML tags like <time> get stripped out, this article is for you.

The real reason <time> gets removed

wp_kses_post() is a sanitization function, not an escaping one.

Its primary job is to protect against XSS (Cross-Site Scripting) by allowing only a predefined set of HTML tags and attributes that WordPress considers safe for post content.

Internally, it works with a strict whitelist.

And here's the key point:

HTML5 semantic tags like <time> are NOT part of the default whitelist.

So when you pass content like this:

<time datetime="2025-01-01">January 1, 2025</time>

through wp_kses_post(), WordPress doesn't "partially clean" it — it removes the tag entirely.

This is expected behavior, not a bug.

"But <span> works — why?"

Because <span> is whitelisted by default.

WordPress' whitelist was designed long before HTML5 semantics became common, and it favors generic, low-risk tags over newer semantic ones.

That's why replacing <time> with <span> appears to "fix" the issue — but at the cost of semantics, accessibility, and structured data.

The wrong way to "fix" it ❌

When developers hit this issue, the usual bad solutions show up:

echo $content; // No sanitization at all

or

wp_kses($content, wp_kses_allowed_html('all'));

Both approaches weaken or bypass XSS protection.

If user-generated content is involved, this is a security risk — plain and simple.

The correct solution: extend the whitelist

WordPress gives us a first-class extension point for this exact scenario:

wp_kses_allowed_html

Here's the safe and intended way to allow <time>:

add_filter('wp_kses_allowed_html', function ($tags, $context) {
    if ($context === 'post') {
        $tags['time'] = [
            'datetime' => true,
            'class'    => true,
        ];
    }
  return $tags;
  }, 10, 2);

Why this works

  • It only affects the post context
  • It keeps the whitelist model intact
  • It allows only explicitly approved attributes
  • XSS protection remains fully enabled

Now this survives sanitization:

<time datetime="2025-01-01" class="published-date">
    January 1, 2025
</time>

A related gotcha: <iframe> is also removed

This often surprises people.

Yes — wp_kses_post() removes <iframe> as well.

That's intentional. Iframes can load external content and introduce serious security risks.

WordPress prefers oEmbed, where it generates safe iframes internally instead of trusting user input.

If you do need to allow iframes (e.g. YouTube embeds in trusted content), you should whitelist them explicitly, just like <time>.

One function or multiple filters?

If you're allowing multiple tags (<time>, <iframe>, etc.) in the same context, it's usually better to keep them in a single filter callback:

  • Easier to audit
  • Easier to review from a security perspective
  • Clearer "HTML policy" in one place

Example:

add_filter('wp_kses_allowed_html', function ($tags, $context) {
    if ($context === 'post') {
        $tags['time'] = [
                    'datetime' => true,
                    'class'    => true,
                ];
                $tags['iframe'] = [
                    'src'             => true,
                    'width'           => true,
                    'height'          => true,
                    'frameborder'     => true,
                    'allowfullscreen' => true,
                    'allow'           => true,
                ];
            }
            return $tags;
        }, 10, 2);

Same security model. Better maintainability.

Key takeaway

wp_kses_post() is not your enemy — it's doing exactly what it's designed to do.

The mistake is trying to bypass it, instead of extending it properly.

Sanitize by whitelisting. Extend — don't disable.

If you treat wp_kses_post() as a security boundary instead of an inconvenience, WordPress gives you all the tools you need.