term_id, 'rank_math_title', true ); if ( ! empty( $current_term_title_template ) ) { $title_template = $current_term_title_template; } if ( ! empty( $title_template ) ) { $title = RankMath\Helper::replace_vars( $title_template, $term ); } } } // Tag Page if ( $slug = get_query_var( 'aiopod_tag' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) { // Get the global title template for the tag pages $title_template = RankMath\Helper::get_settings( 'titles.tax_aiopod_tags_title' ); // Get the title template for the current tag page (if available) $current_term_title_template = get_term_meta( $term->term_id, 'rank_math_title', true ); if ( ! empty( $current_term_title_template ) ) { $title_template = $current_term_title_template; } if ( ! empty( $title_template ) ) { $title = RankMath\Helper::replace_vars( $title_template, $term ); } } } // Participant Page if ( $slug = get_query_var( 'aiopod_participant' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) { // Get the global title template for the participant pages $title_template = RankMath\Helper::get_settings( 'titles.tax_aiopod_participants_title' ); // Get the title template for the current participant page (if available) $current_term_title_template = get_term_meta( $term->term_id, 'rank_math_title', true ); if ( ! empty( $current_term_title_template ) ) { $title_template = $current_term_title_template; } if ( ! empty( $title_template ) ) { $title = RankMath\Helper::replace_vars( $title_template, $term ); } } } return $title; } /** * Updates the page description on the plugin pages. * * @since 1.0.0 * @param string $description Default page description. * @return string $description The filtered page description. */ public function meta_description( $description ) { global $post; // Category Page if ( $slug = get_query_var( 'aiopod_category' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) { // Get the global description template for the category pages $description_template = RankMath\Helper::get_settings( 'titles.tax_aiopod_categories_description' ); // Get the description template for the current category page (if available) $current_term_description_template = get_term_meta( $term->term_id, 'rank_math_description', true ); if ( ! empty( $current_term_description_template ) ) { $description_template = $current_term_description_template; } if ( ! empty( $description_template ) ) { $description = RankMath\Helper::replace_vars( $description_template, $term ); } } } // Tag Page if ( $slug = get_query_var( 'aiopod_tag' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) { // Get the global description template for the tag pages $description_template = RankMath\Helper::get_settings( 'titles.tax_aiopod_tags_description' ); // Get the description template for the current tag page (if available) $current_term_description_template = get_term_meta( $term->term_id, 'rank_math_description', true ); if ( ! empty( $current_term_description_template ) ) { $description_template = $current_term_description_template; } if ( ! empty( $description_template ) ) { $description = RankMath\Helper::replace_vars( $description_template, $term ); } } } // Participant Page if ( $slug = get_query_var( 'aiopod_participant' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) { // Get the global description template for the participant pages $description_template = RankMath\Helper::get_settings( 'titles.tax_aiopod_participants_description' ); // Get the description template for the current participant page (if available) $current_term_description_template = get_term_meta( $term->term_id, 'rank_math_description', true ); if ( ! empty( $current_term_description_template ) ) { $description_template = $current_term_description_template; } if ( ! empty( $description_template ) ) { $description = RankMath\Helper::replace_vars( $description_template, $term ); } } } return $description; } /** * Updates the canonical URL on the plugin pages. * * @since 1.0.0 * @param string $canonical_url The canonical URL. * @return string $canonical_url The filtered canonical URL. */ public function canonical_url( $canonical_url ) { // Category Page if ( $slug = get_query_var( 'aiopod_category' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) { $canonical_url = aiopod_get_category_page_url( $term ); } } // Tag Page if ( $slug = get_query_var( 'aiopod_tag' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) { $canonical_url = aiopod_get_tag_page_url( $term ); } } // Participant Page if ( $slug = get_query_var( 'aiopod_participant' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) { $canonical_url = aiopod_get_participant_page_url( $term ); } } return $canonical_url; } /** * Updates the Open Graph image URL on the plugin pages. * * @since 1.0.0 * @param string $image_url The Image URL. * @return string $image_url The filtered image URL. */ public function opengraph_image_url( $image_url ) { // Category Page if ( $slug = get_query_var( 'aiopod_category' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) { // Get the opengraph image URL for the current category page (if available) $opengraph_image_url = get_term_meta( $term->term_id, 'rank_math_facebook_image', true ); if ( ! empty( $opengraph_image_url ) ) { $image_url = $opengraph_image_url; } else { // Get the actual image URL for the current category page (if available) $image_data = aiopod_get_image( array( 'term_id' => $term->term_id, 'size' => 'large', 'alt' => '' ) ); if ( ! empty( $image_data['src'] ) ) { $image_url = $image_data['src']; } } } } // Tag Page if ( $slug = get_query_var( 'aiopod_tag' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) { // Get the opengraph image URL for the current tag page (if available) $opengraph_image_url = get_term_meta( $term->term_id, 'rank_math_facebook_image', true ); if ( ! empty( $opengraph_image_url ) ) { $image_url = $opengraph_image_url; } } } // Participant Page if ( $slug = get_query_var( 'aiopod_participant' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) { // Get the opengraph image URL for the current participant page (if available) $opengraph_image_url = get_term_meta( $term->term_id, 'rank_math_facebook_image', true ); if ( ! empty( $opengraph_image_url ) ) { $image_url = $opengraph_image_url; } } } return $image_url; } /** * Filters the Breadcrumb items. * * @since 1.0.0 * @param array $crumbs Array of crumbs. * @return array $crumbs The filtered array of crumbs. */ public function breadcrumb_items( $crumbs ) { // Explicit CPT → option key mapping (future-proof) $archive_map = array( 'aiopod_podcasts' => 'podcast_archive_page', 'aiopod_episodes' => 'episode_archive_page' ); // Single Podcast and Episode Pages if ( is_singular( array_keys( $archive_map ) ) ) { // The queried object, not the global $post, so a theme calling the breadcrumbs from // inside a secondary loop still gets the trail for the page being viewed return $this->add_archive_page_crumbs( $crumbs, get_post_type( get_queried_object_id() ), $archive_map ); } // On every page the plugin manages, fill in any ancestor Rank Math has not supplied // itself, so a page nested under the podcast archive reads as the whole path to it $pages_settings = aiopod_get_option( 'aiopod_pages_settings' ); $page_id = get_queried_object_id(); $plugin_pages = array( 'category', 'tag', 'participant', 'podcast_archive_page', 'episode_archive_page' ); foreach ( $plugin_pages as $page_key ) { if ( ! empty( $pages_settings[ $page_key ] ) && $page_id === (int) $pages_settings[ $page_key ] ) { $crumbs = $this->add_page_ancestor_crumbs( $crumbs, $page_id ); break; } } // Category Page if ( $slug = get_query_var( 'aiopod_category' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_categories' ) ) { // Include the parent categories if available if ( $ancestors = get_ancestors( $term->term_id, 'aiopod_categories' ) ) { $ancestors = array_reverse( $ancestors ); foreach ( $ancestors as $term_id ) { if ( $parent_term = get_term_by( 'term_id', $term_id, 'aiopod_categories' ) ) { $crumbs[] = array( 0 => $parent_term->name, 1 => aiopod_get_category_page_url( $parent_term ) ); } } } // Include the current category page. The URL matters: Rank Math leaves a crumb // out of its BreadcrumbList entirely when it has no URL $crumbs[] = array( 0 => $term->name, 1 => aiopod_get_category_page_url( $term ) ); } } // Tag Page if ( $slug = get_query_var( 'aiopod_tag' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_tags' ) ) { $crumbs[] = array( 0 => $term->name, 1 => aiopod_get_tag_page_url( $term ) ); } } // Participant Page if ( $slug = get_query_var( 'aiopod_participant' ) ) { if ( $term = get_term_by( 'slug', $slug, 'aiopod_participants' ) ) { $crumbs[] = array( 0 => $term->name, 1 => aiopod_get_participant_page_url( $term ) ); } } return $crumbs; } /** * Build the trail of a single podcast or episode. * * An episode reads `Home > Podcasts > Show > Episode`: the show it belongs to is the useful step, * and the episode archive is not on the way to it, so that crumb is replaced rather than kept. * Rank Math already walks `post_parent`, which is where an episode keeps its show, so the show * crumb is usually there already and the dedupe leaves it where it is. * * @access private * @since 1.0.4 * @param array $crumbs Array of crumbs. * @param string $post_type Post type of the single being viewed. * @param array $archive_map CPT → archive page option key. * @return array $crumbs The filtered array of crumbs. */ private function add_archive_page_crumbs( $crumbs, $post_type, $archive_map ) { if ( empty( $archive_map[ $post_type ] ) ) { return $crumbs; } if ( 'aiopod_episodes' === $post_type ) { $episode_id = get_queried_object_id(); $podcast_id = aiopod_get_episode_podcast_id( $episode_id ); // An episode with no published show of its own keeps the episode archive trail if ( $podcast_id ) { $chain = $this->get_archive_crumbs( 'aiopod_podcasts', $archive_map ); if ( ! empty( $chain ) ) { $chain[] = $this->get_page_crumb( $podcast_id ); $crumbs = $this->remove_archive_crumb( $crumbs, $post_type ); return $this->insert_crumb_chain( $crumbs, $chain ); } } $crumbs = $this->remove_unpublished_podcast_crumb( $crumbs, $episode_id ); } $chain = $this->get_archive_crumbs( $post_type, $archive_map, true ); if ( empty( $chain ) ) { return $crumbs; } // Where Rank Math drew its own archive crumb, it is left alone and only the URL is redirected $page_url = $chain[ count( $chain ) - 1 ][1]; $archive_url = get_post_type_archive_link( $post_type ); if ( $archive_url ) { foreach ( $crumbs as $index => $crumb ) { if ( ! empty( $crumb[1] ) && untrailingslashit( $crumb[1] ) === untrailingslashit( $archive_url ) ) { $crumbs[ $index ][1] = $page_url; return $crumbs; } } } return $this->insert_crumb_chain( $crumbs, $chain ); } /** * Build the crumbs that stand for a post type's archive. * * The assigned archive page where there is one — with its own ancestors, so a nested page reads * as the whole path to it — and the post type's own archive where there is not. * * @access private * @since 1.0.4 * @param string $post_type Post type whose archive is wanted. * @param array $archive_map CPT → archive page option key. * @param bool $page_only Return nothing rather than fall back to the post type archive. * @return array Crumbs, outermost first. Empty when there is no archive to point at. */ private function get_archive_crumbs( $post_type, $archive_map, $page_only = false ) { $chain = array(); $pages_settings = aiopod_get_option( 'aiopod_pages_settings' ); $option_key = isset( $archive_map[ $post_type ] ) ? $archive_map[ $post_type ] : ''; if ( $option_key && ! empty( $pages_settings[ $option_key ] ) ) { $page_id = (int) $pages_settings[ $option_key ]; $page = get_post( $page_id ); if ( $page && 'publish' === $page->post_status && get_permalink( $page_id ) ) { foreach ( array_reverse( get_post_ancestors( $page_id ) ) as $ancestor_id ) { $ancestor = get_post( $ancestor_id ); if ( $ancestor && 'publish' === $ancestor->post_status ) { $chain[] = $this->get_page_crumb( $ancestor_id ); } } $chain[] = $this->get_page_crumb( $page_id ); return $chain; } } if ( $page_only ) { return $chain; } $url = get_post_type_archive_link( $post_type ); $obj = get_post_type_object( $post_type ); if ( ! $url || ! $obj ) { return $chain; } // The same label Rank Math uses for a post type archive crumb of its own $chain[] = array( 0 => $obj->labels->name, 1 => $url ); return $chain; } /** * Drop the post type archive crumb Rank Math drew for a post type. * * Rank Math marks nothing, so the archive URL is what identifies the crumb. * * @access private * @since 1.0.4 * @param array $crumbs Array of crumbs. * @param string $post_type Post type whose archive crumb is not wanted. * @return array $crumbs The filtered array of crumbs. */ private function remove_archive_crumb( $crumbs, $post_type ) { $archive_url = get_post_type_archive_link( $post_type ); if ( ! $archive_url ) { return $crumbs; } foreach ( $crumbs as $index => $crumb ) { if ( ! empty( $crumb[1] ) && untrailingslashit( $crumb[1] ) === untrailingslashit( $archive_url ) ) { unset( $crumbs[ $index ] ); return array_values( $crumbs ); } } return $crumbs; } /** * Take an unpublished show out of an episode's trail. * * Both SEO plugins build a post's ancestors from `post_parent` without checking status, so a * podcast reverted to draft still appears in the breadcrumbs of its published episodes, named * and linked to an address that answers 404. Declining to add the crumb is not enough — it is * already there, and has to be taken out. * * @access private * @since 1.0.4 * @param array $crumbs Array of crumbs. * @param int $episode_id ID of the episode being viewed. * @return array $crumbs The filtered array of crumbs. */ private function remove_unpublished_podcast_crumb( $crumbs, $episode_id ) { $podcast_id = (int) get_post_field( 'post_parent', $episode_id ); // `get_post( 0 )` falls back to the global post, so the parent has to be checked first if ( $podcast_id <= 0 ) { return $crumbs; } $podcast = get_post( $podcast_id ); if ( ! $podcast || 'aiopod_podcasts' !== $podcast->post_type || 'publish' === $podcast->post_status ) { return $crumbs; } $index = $this->find_crumb( $crumbs, $this->get_page_crumb( $podcast_id ) ); if ( null === $index ) { return $crumbs; } unset( $crumbs[ $index ] ); return array_values( $crumbs ); } /** * Fill in the ancestors of one of the plugin's overview pages. * * On the category, tag and participant pages the plugin takes the trail over, so any ancestor * Rank Math has not supplied itself has to be filled in here. * * @access private * @since 1.0.4 * @param array $crumbs Array of crumbs. * @param int $page_id ID of the page being viewed. * @return array $crumbs The filtered array of crumbs. */ private function add_page_ancestor_crumbs( $crumbs, $page_id ) { $chain = array(); foreach ( array_reverse( get_post_ancestors( $page_id ) ) as $ancestor_id ) { $ancestor = get_post( $ancestor_id ); if ( $ancestor && 'publish' === $ancestor->post_status ) { $chain[] = $this->get_page_crumb( $ancestor_id ); } } return $this->insert_crumb_chain( $crumbs, $chain ); } /** * Insert a top-down chain of crumbs into the trail, skipping any already in it. * * The cursor is what keeps a partly-built trail in order: a crumb that is already present moves * the insertion point to just after itself, so anything still missing below it lands in the * right place rather than all of it being stacked behind the home crumb. * * @access private * @since 1.0.4 * @param array $crumbs Array of crumbs. * @param array $chain Crumbs to insert, outermost first. * @return array $crumbs The filtered array of crumbs. */ private function insert_crumb_chain( $crumbs, $chain ) { $position = $this->get_crumb_insert_position( $crumbs ); foreach ( $chain as $crumb ) { $existing = $this->find_crumb( $crumbs, $crumb ); if ( null !== $existing ) { $position = $existing + 1; continue; } array_splice( $crumbs, $position, 0, array( $crumb ) ); $position++; } return $crumbs; } /** * Build a Rank Math breadcrumb for a post or page. * * @access private * @since 1.0.4 * @param int $post_id ID of the post or page. * @return array Breadcrumb for it. */ private function get_page_crumb( $post_id ) { // Rank Math's own Breadcrumb Title field, so an inserted crumb reads the way Rank Math // would have titled it had it built the crumb itself $text = get_post_meta( $post_id, 'rank_math_breadcrumb_title', true ); if ( empty( $text ) ) { $text = get_the_title( $post_id ); } return array( 0 => $text, 1 => get_permalink( $post_id ) ); } /** * Find a crumb already in the trail. * * Rank Math crumbs carry no post ID, so the URL is all there is to match on. * * @access private * @since 1.0.4 * @param array $crumbs Array of crumbs. * @param array $needle Crumb to look for. * @return int|null Index of the crumb, or null when it is not in the trail. */ private function find_crumb( $crumbs, $needle ) { if ( empty( $needle[1] ) ) { return null; } foreach ( $crumbs as $index => $crumb ) { if ( ! empty( $crumb[1] ) && untrailingslashit( $crumb[1] ) === untrailingslashit( $needle[1] ) ) { return $index; } } return null; } /** * Find where an inserted crumb belongs — after the home crumb, where there is one. * * @access private * @since 1.0.4 * @param array $crumbs Array of crumbs. * @return int Index to insert at. */ private function get_crumb_insert_position( $crumbs ) { if ( empty( $crumbs ) ) { return 0; } // Rank Math draws its home crumb first when the setting is on, and lets an owner point it // anywhere — so the setting is the only reliable test, not the crumb's URL if ( class_exists( 'RankMath\Helper' ) ) { return \RankMath\Helper::get_settings( 'general.breadcrumbs_home' ) ? 1 : 0; } if ( ! empty( $crumbs[0][1] ) && untrailingslashit( $crumbs[0][1] ) === untrailingslashit( home_url() ) ) { return 1; } return 0; } }