PodcastEpisode structured data
Output valid PodcastEpisode JSON-LD in the head of single episode pages, built from post data and safely encoded.
WordPressSEOjson-ldschema-orgpodcaststructured-data
Code
<?php
/**
* PodcastEpisode JSON-LD.
*
* Expects post type "acme_episode" with meta: acme_audio_url, acme_duration (seconds).
*
* @package Acme_Theme
*/
defined( 'ABSPATH' ) || exit;
function acme_episode_schema(): void {
if ( ! is_singular( 'acme_episode' ) ) {
return;
}
$post = get_queried_object();
$audio = esc_url_raw( (string) get_post_meta( $post->ID, 'acme_audio_url', true ) );
$duration = absint( get_post_meta( $post->ID, 'acme_duration', true ) );
$data = array(
'@context' => 'https://schema.org',
'@type' => 'PodcastEpisode',
'url' => get_permalink( $post ),
'name' => get_the_title( $post ),
'description' => wp_strip_all_tags( get_the_excerpt( $post ) ),
'datePublished' => get_post_time( 'c', true, $post ),
'partOfSeries' => array(
'@type' => 'PodcastSeries',
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
),
);
if ( $audio ) {
$data['associatedMedia'] = array(
'@type' => 'AudioObject',
'contentUrl' => $audio,
'encodingFormat' => 'audio/mpeg',
);
}
if ( $duration > 0 ) {
// ISO 8601 duration: PT1H2M3S.
$data['timeRequired'] = sprintf(
'PT%dH%dM%dS',
intdiv( $duration, 3600 ),
intdiv( $duration % 3600, 60 ),
$duration % 60
);
}
if ( has_post_thumbnail( $post ) ) {
$data['image'] = get_the_post_thumbnail_url( $post, 'large' );
}
// JSON_HEX_TAG stops "</script>" in titles from closing the tag early.
printf(
'<script type="application/ld+json">%s</script>' . "\n",
wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP )
);
}
add_action( 'wp_head', 'acme_episode_schema', 20 );
How it works
What it solves
Search engines and podcast directories can understand an episode page far better when it carries structured data. Hand-written JSON in a template breaks on quotes and special characters, so this builds an array and encodes it properly.
How it works
The function runs in wp_head only on single episode pages. It collects the title, excerpt, publish time in ISO 8601, the series and, when present, an AudioObject pointing at the file. The duration in seconds is converted to a valid ISO duration. Values from post meta pass through esc_url_raw or absint, and the final array is serialised with wp_json_encode using the tag-hex flags.
Because the JSON is the only thing echoed, there is nothing to inject into.
Adapting it
Rename the post type and meta keys to yours. Add episodeNumber, seasonNumber or author the same way. If an SEO plugin already prints a graph, merge into its filter instead of adding a second block. Test the output with a rich results validator before relying on it.
Pitfalls
- Titles with a closing script tag would break naive output; the hex flags prevent that.
- Empty properties should be omitted, not printed blank.
- Dates must be ISO 8601 with a timezone;
get_post_time( 'c', true )gives UTC. - Only describe content that is visible on the page.
How to use it
Save as inc/schema-episode.php and require it from functions.php.
Store the audio URL and the duration in seconds in the two meta fields.
View the page source of an episode and paste the block into a structured data validator.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.