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 allor
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
postcontext - 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.