rx-theme, focused on “Article Level” with admin UI, REST support, rewrite rules, SEO helpers, schema hooks, filters, shortcodes, and safe defaults.Below is a very advanced inc/taxonomies/article-level.php file for your rx-theme.
It registers an Article Level taxonomy, adds admin fields, REST API support, term meta, colors/icons, admin columns, default terms, frontend badges, shortcodes, SEO/meta helpers, schema JSON-LD, admin filters, rewrite support, and helper functions.
WordPress recommends registering custom taxonomies on the init hook, and show_in_rest is required for Block Editor/REST support. Term meta should be registered with register_term_meta() for safer REST/API access.
Create this file:
rx-theme/inc/taxonomies/article-level.php
<?php
/**
* RX Theme - Article Level Taxonomy
*
* File: inc/taxonomies/article-level.php
*
* Features:
* - Registers Article Level taxonomy.
* - Supports posts and custom post types.
* - REST API and Gutenberg support.
* - SEO-friendly rewrite URLs.
* - Default terms installer.
* - Term meta: color, icon, order, reading age, score, SEO title, SEO description,
* schema name, schema description, featured flag, noindex flag, canonical URL,
* hero image ID.
* - Admin add/edit term fields.
* - Admin taxonomy columns.
* - Admin post-list filter dropdown.
* - Frontend badge helpers.
* - Shortcodes.
* - Body/post classes.
* - Archive SEO meta.
* - JSON-LD schema output.
* - Safe sanitization and escaping.
*
* @package RX_Theme
*/
defined( 'ABSPATH' ) || exit;
if ( ! defined( 'RX_ARTICLE_LEVEL_TAXONOMY' ) ) {
define( 'RX_ARTICLE_LEVEL_TAXONOMY', 'article_level' );
}
if ( ! defined( 'RX_ARTICLE_LEVEL_VERSION' ) ) {
define( 'RX_ARTICLE_LEVEL_VERSION', '1.0.0' );
}
if ( ! class_exists( 'RX_Article_Level_Taxonomy' ) ) :
final class RX_Article_Level_Taxonomy {
/**
* Taxonomy key.
*
* @var string
*/
const TAXONOMY = RX_ARTICLE_LEVEL_TAXONOMY;
/**
* Meta keys.
*
* @var array
*/
private static $meta_keys = array(
'color' => '_rx_article_level_color',
'background_color' => '_rx_article_level_background_color',
'icon' => '_rx_article_level_icon',
'order' => '_rx_article_level_order',
'reading_age' => '_rx_article_level_reading_age',
'difficulty_score' => '_rx_article_level_difficulty_score',
'estimated_minutes' => '_rx_article_level_estimated_minutes',
'seo_title' => '_rx_article_level_seo_title',
'seo_description' => '_rx_article_level_seo_description',
'schema_name' => '_rx_article_level_schema_name',
'schema_description' => '_rx_article_level_schema_description',
'featured' => '_rx_article_level_featured',
'noindex' => '_rx_article_level_noindex',
'canonical_url' => '_rx_article_level_canonical_url',
'hero_image_id' => '_rx_article_level_hero_image_id',
);
/**
* Boot class.
*
* @return void
*/
public static function boot() {
add_action( 'init', array( __CLASS__, 'register_taxonomy' ), 5 );
add_action( 'init', array( __CLASS__, 'register_term_meta' ), 6 );
add_action( 'init', array( __CLASS__, 'maybe_insert_default_terms' ), 20 );
add_action( self::TAXONOMY . '_add_form_fields', array( __CLASS__, 'add_term_fields' ) );
add_action( self::TAXONOMY . '_edit_form_fields', array( __CLASS__, 'edit_term_fields' ), 10, 2 );
add_action( 'created_' . self::TAXONOMY, array( __CLASS__, 'save_term_meta' ) );
add_action( 'edited_' . self::TAXONOMY, array( __CLASS__, 'save_term_meta' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'admin_assets' ) );
add_filter( 'manage_edit-' . self::TAXONOMY . '_columns', array( __CLASS__, 'admin_columns' ) );
add_filter( 'manage_' . self::TAXONOMY . '_custom_column', array( __CLASS__, 'admin_column_content' ), 10, 3 );
add_filter( 'manage_edit-' . self::TAXONOMY . '_sortable_columns', array( __CLASS__, 'sortable_columns' ) );
add_action( 'restrict_manage_posts', array( __CLASS__, 'admin_filter_dropdown' ) );
add_filter( 'parse_query', array( __CLASS__, 'admin_filter_query' ) );
add_filter( 'term_link', array( __CLASS__, 'term_link_filter' ), 10, 3 );
add_filter( 'get_the_archive_title', array( __CLASS__, 'archive_title' ) );
add_filter( 'document_title_parts', array( __CLASS__, 'document_title_parts' ) );
add_action( 'wp_head', array( __CLASS__, 'archive_meta_tags' ), 4 );
add_action( 'wp_head', array( __CLASS__, 'schema_json_ld' ), 30 );
add_filter( 'body_class', array( __CLASS__, 'body_classes' ) );
add_filter( 'post_class', array( __CLASS__, 'post_classes' ), 10, 3 );
add_shortcode( 'rx_article_level', array( __CLASS__, 'shortcode_article_level' ) );
add_shortcode( 'rx_article_level_list', array( __CLASS__, 'shortcode_article_level_list' ) );
add_action( 'save_post', array( __CLASS__, 'maybe_assign_default_level' ), 20, 3 );
add_action( 'after_switch_theme', array( __CLASS__, 'flush_rewrite_rules_on_theme_switch' ) );
}
/**
* Register taxonomy.
*
* @return void
*/
public static function register_taxonomy() {
$object_types = self::get_supported_post_types();
$labels = array(
'name' => _x( 'Article Levels', 'taxonomy general name', 'rx-theme' ),
'singular_name' => _x( 'Article Level', 'taxonomy singular name', 'rx-theme' ),
'search_items' => __( 'Search Article Levels', 'rx-theme' ),
'popular_items' => __( 'Popular Article Levels', 'rx-theme' ),
'all_items' => __( 'All Article Levels', 'rx-theme' ),
'parent_item' => __( 'Parent Article Level', 'rx-theme' ),
'parent_item_colon' => __( 'Parent Article Level:', 'rx-theme' ),
'edit_item' => __( 'Edit Article Level', 'rx-theme' ),
'view_item' => __( 'View Article Level', 'rx-theme' ),
'update_item' => __( 'Update Article Level', 'rx-theme' ),
'add_new_item' => __( 'Add New Article Level', 'rx-theme' ),
'new_item_name' => __( 'New Article Level Name', 'rx-theme' ),
'separate_items_with_commas' => __( 'Separate article levels with commas', 'rx-theme' ),
'add_or_remove_items' => __( 'Add or remove article levels', 'rx-theme' ),
'choose_from_most_used' => __( 'Choose from the most used article levels', 'rx-theme' ),
'not_found' => __( 'No article levels found.', 'rx-theme' ),
'no_terms' => __( 'No article levels', 'rx-theme' ),
'filter_by_item' => __( 'Filter by article level', 'rx-theme' ),
'items_list_navigation' => __( 'Article levels list navigation', 'rx-theme' ),
'items_list' => __( 'Article levels list', 'rx-theme' ),
'most_used' => __( 'Most Used', 'rx-theme' ),
'back_to_items' => __( '← Back to Article Levels', 'rx-theme' ),
'menu_name' => __( 'Article Levels', 'rx-theme' ),
'name_admin_bar' => __( 'Article Level', 'rx-theme' ),
);
$args = array(
'labels' => $labels,
'description' => __( 'Classify articles by reader difficulty, professional depth, or knowledge level.', 'rx-theme' ),
'public' => true,
'publicly_queryable' => true,
'hierarchical' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => false,
'show_in_quick_edit' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rest_base' => 'article-levels',
'rest_namespace' => 'wp/v2',
'query_var' => self::TAXONOMY,
'capabilities' => array(
'manage_terms' => 'manage_categories',
'edit_terms' => 'manage_categories',
'delete_terms' => 'manage_categories',
'assign_terms' => 'edit_posts',
),
'rewrite' => array(
'slug' => self::get_rewrite_slug(),
'with_front' => false,
'hierarchical' => true,
'ep_mask' => EP_NONE,
),
'default_term' => array(
'name' => __( 'General Reader', 'rx-theme' ),
'slug' => 'general-reader',
'description' => __( 'Simple, accessible content for general readers.', 'rx-theme' ),
),
'sort' => true,
'meta_box_cb' => 'post_categories_meta_box',
);
$args = apply_filters( 'rx_article_level_taxonomy_args', $args, $object_types );
register_taxonomy( self::TAXONOMY, $object_types, $args );
}
/**
* Supported post types.
*
* @return array
*/
public static function get_supported_post_types() {
$post_types = array( 'post' );
$optional = array(
'article',
'medical_article',
'condition',
'disease',
'health_article',
'rx_article',
'guide',
'blog',
);
foreach ( $optional as $post_type ) {
if ( post_type_exists( $post_type ) ) {
$post_types[] = $post_type;
}
}
return apply_filters( 'rx_article_level_supported_post_types', array_unique( $post_types ) );
}
/**
* Rewrite slug.
*
* @return string
*/
public static function get_rewrite_slug() {
return apply_filters( 'rx_article_level_rewrite_slug', 'article-level' );
}
/**
* Register term meta.
*
* @return void
*/
public static function register_term_meta() {
$fields = array(
self::$meta_keys['color'] => array(
'type' => 'string',
'description' => __( 'Text color for article level badge.', 'rx-theme' ),
'sanitize_callback' => array( __CLASS__, 'sanitize_color' ),
'default' => '#ffffff',
),
self::$meta_keys['background_color'] => array(
'type' => 'string',
'description' => __( 'Background color for article level badge.', 'rx-theme' ),
'sanitize_callback' => array( __CLASS__, 'sanitize_color' ),
'default' => '#0f766e',
),
self::$meta_keys['icon'] => array(
'type' => 'string',
'description' => __( 'Small icon, emoji, or dashicon name.', 'rx-theme' ),
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
),
self::$meta_keys['order'] => array(
'type' => 'integer',
'description' => __( 'Custom display order.', 'rx-theme' ),
'sanitize_callback' => 'absint',
'default' => 0,
),
self::$meta_keys['reading_age'] => array(
'type' => 'string',
'description' => __( 'Suggested reader age or grade level.', 'rx-theme' ),
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
),
self::$meta_keys['difficulty_score'] => array(
'type' => 'integer',
'description' => __( 'Difficulty score from 1 to 10.', 'rx-theme' ),
'sanitize_callback' => array( __CLASS__, 'sanitize_score' ),
'default' => 1,
),
self::$meta_keys['estimated_minutes'] => array(
'type' => 'integer',
'description' => __( 'Estimated reading minutes for this level.', 'rx-theme' ),
'sanitize_callback' => 'absint',
'default' => 0,
),
self::$meta_keys['seo_title'] => array(
'type' => 'string',
'description' => __( 'Custom SEO title for article level archive.', 'rx-theme' ),
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
),
self::$meta_keys['seo_description'] => array(
'type' => 'string',
'description' => __( 'Custom SEO description for article level archive.', 'rx-theme' ),
'sanitize_callback' => 'sanitize_textarea_field',
'default' => '',
),
self::$meta_keys['schema_name'] => array(
'type' => 'string',
'description' => __( 'Schema display name.', 'rx-theme' ),
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
),
self::$meta_keys['schema_description'] => array(
'type' => 'string',
'description' => __( 'Schema display description.', 'rx-theme' ),
'sanitize_callback' => 'sanitize_textarea_field',
'default' => '',
),
self::$meta_keys['featured'] => array(
'type' => 'boolean',
'description' => __( 'Feature this article level.', 'rx-theme' ),
'sanitize_callback' => array( __CLASS__, 'sanitize_bool' ),
'default' => false,
),
self::$meta_keys['noindex'] => array(
'type' => 'boolean',
'description' => __( 'Ask search engines not to index this archive.', 'rx-theme' ),
'sanitize_callback' => array( __CLASS__, 'sanitize_bool' ),
'default' => false,
),
self::$meta_keys['canonical_url'] => array(
'type' => 'string',
'description' => __( 'Custom canonical URL.', 'rx-theme' ),
'sanitize_callback' => 'esc_url_raw',
'default' => '',
),
self::$meta_keys['hero_image_id'] => array(
'type' => 'integer',
'description' => __( 'Hero image attachment ID.', 'rx-theme' ),
'sanitize_callback' => 'absint',
'default' => 0,
),
);
foreach ( $fields as $key => $args ) {
register_term_meta(
self::TAXONOMY,
$key,
array_merge(
array(
'single' => true,
'show_in_rest' => true,
'auth_callback' => function() {
return current_user_can( 'edit_posts' );
},
),
$args
)
);
}
}
/**
* Insert default terms once.
*
* @return void
*/
public static function maybe_insert_default_terms() {
if ( get_option( 'rx_article_level_terms_installed' ) ) {
return;
}
$terms = array(
array(
'name' => 'General Reader',
'slug' => 'general-reader',
'description' => 'Simple, friendly, and easy-to-understand article for all readers.',
'meta' => array(
'color' => '#ffffff',
'background_color' => '#0f766e',
'icon' => '📘',
'order' => 10,
'reading_age' => 'General public',
'difficulty_score' => 1,
),
),
array(
'name' => 'Beginner',
'slug' => 'beginner',
'description' => 'Basic article for new learners and non-specialist readers.',
'meta' => array(
'color' => '#ffffff',
'background_color' => '#2563eb',
'icon' => '🌱',
'order' => 20,
'reading_age' => 'Beginner reader',
'difficulty_score' => 2,
),
),
array(
'name' => 'Intermediate',
'slug' => 'intermediate',
'description' => 'Moderate-level article with more explanation and technical detail.',
'meta' => array(
'color' => '#ffffff',
'background_color' => '#7c3aed',
'icon' => '📗',
'order' => 30,
'reading_age' => 'Intermediate reader',
'difficulty_score' => 5,
),
),
array(
'name' => 'Advanced',
'slug' => 'advanced',
'description' => 'Detailed article for advanced learners, clinicians, and professionals.',
'meta' => array(
'color' => '#ffffff',
'background_color' => '#be123c',
'icon' => '🧠',
'order' => 40,
'reading_age' => 'Advanced reader',
'difficulty_score' => 8,
),
),
array(
'name' => 'Professional',
'slug' => 'professional',
'description' => 'Professional-level article with clinical, scientific, or technical depth.',
'meta' => array(
'color' => '#ffffff',
'background_color' => '#111827',
'icon' => '⚕️',
'order' => 50,
'reading_age' => 'Professional reader',
'difficulty_score' => 9,
),
),
array(
'name' => 'Research Level',
'slug' => 'research-level',
'description' => 'Research-focused article with evidence, citations, mechanisms, and technical depth.',
'meta' => array(
'color' => '#ffffff',
'background_color' => '#92400e',
'icon' => '🔬',
'order' => 60,
'reading_age' => 'Research reader',
'difficulty_score' => 10,
),
),
);
foreach ( $terms as $term ) {
if ( term_exists( $term['slug'], self::TAXONOMY ) ) {
continue;
}
$result = wp_insert_term(
$term['name'],
self::TAXONOMY,
array(
'slug' => $term['slug'],
'description' => $term['description'],
)
);
if ( is_wp_error( $result ) || empty( $result['term_id'] ) ) {
continue;
}
foreach ( $term['meta'] as $meta_key => $value ) {
if ( isset( self::$meta_keys[ $meta_key ] ) ) {
update_term_meta( $result['term_id'], self::$meta_keys[ $meta_key ], $value );
}
}
}
update_option( 'rx_article_level_terms_installed', 1, false );
}
/**
* Add term fields.
*
* @return void
*/
public static function add_term_fields() {
wp_nonce_field( 'rx_article_level_save_meta', 'rx_article_level_nonce' );
?>
<div class="form-field rx-term-field">
<label for="rx_article_level_color"><?php esc_html_e( 'Text Color', 'rx-theme' ); ?></label>
<input type="text" name="rx_article_level_meta[color]" id="rx_article_level_color" value="#ffffff" class="rx-color-field" />
<p><?php esc_html_e( 'Badge text color. Example: #ffffff.', 'rx-theme' ); ?></p>
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_background_color"><?php esc_html_e( 'Background Color', 'rx-theme' ); ?></label>
<input type="text" name="rx_article_level_meta[background_color]" id="rx_article_level_background_color" value="#0f766e" class="rx-color-field" />
<p><?php esc_html_e( 'Badge background color. Example: #0f766e.', 'rx-theme' ); ?></p>
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_icon"><?php esc_html_e( 'Icon', 'rx-theme' ); ?></label>
<input type="text" name="rx_article_level_meta[icon]" id="rx_article_level_icon" value="" />
<p><?php esc_html_e( 'Use emoji, small text, or icon name. Example: 📘, 🔬, beginner.', 'rx-theme' ); ?></p>
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_order"><?php esc_html_e( 'Display Order', 'rx-theme' ); ?></label>
<input type="number" name="rx_article_level_meta[order]" id="rx_article_level_order" value="0" min="0" step="1" />
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_reading_age"><?php esc_html_e( 'Reading Age / Audience', 'rx-theme' ); ?></label>
<input type="text" name="rx_article_level_meta[reading_age]" id="rx_article_level_reading_age" value="" />
<p><?php esc_html_e( 'Example: General public, Medical student, Clinician, Researcher.', 'rx-theme' ); ?></p>
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_difficulty_score"><?php esc_html_e( 'Difficulty Score', 'rx-theme' ); ?></label>
<input type="number" name="rx_article_level_meta[difficulty_score]" id="rx_article_level_difficulty_score" value="1" min="1" max="10" step="1" />
<p><?php esc_html_e( 'Use 1 for easiest and 10 for most advanced.', 'rx-theme' ); ?></p>
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_estimated_minutes"><?php esc_html_e( 'Estimated Minutes', 'rx-theme' ); ?></label>
<input type="number" name="rx_article_level_meta[estimated_minutes]" id="rx_article_level_estimated_minutes" value="0" min="0" step="1" />
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_seo_title"><?php esc_html_e( 'SEO Title', 'rx-theme' ); ?></label>
<input type="text" name="rx_article_level_meta[seo_title]" id="rx_article_level_seo_title" value="" />
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_seo_description"><?php esc_html_e( 'SEO Description', 'rx-theme' ); ?></label>
<textarea name="rx_article_level_meta[seo_description]" id="rx_article_level_seo_description" rows="4"></textarea>
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_schema_name"><?php esc_html_e( 'Schema Name', 'rx-theme' ); ?></label>
<input type="text" name="rx_article_level_meta[schema_name]" id="rx_article_level_schema_name" value="" />
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_schema_description"><?php esc_html_e( 'Schema Description', 'rx-theme' ); ?></label>
<textarea name="rx_article_level_meta[schema_description]" id="rx_article_level_schema_description" rows="4"></textarea>
</div>
<div class="form-field rx-term-field">
<label>
<input type="checkbox" name="rx_article_level_meta[featured]" value="1" />
<?php esc_html_e( 'Featured article level', 'rx-theme' ); ?>
</label>
</div>
<div class="form-field rx-term-field">
<label>
<input type="checkbox" name="rx_article_level_meta[noindex]" value="1" />
<?php esc_html_e( 'Noindex this archive', 'rx-theme' ); ?>
</label>
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_canonical_url"><?php esc_html_e( 'Canonical URL', 'rx-theme' ); ?></label>
<input type="url" name="rx_article_level_meta[canonical_url]" id="rx_article_level_canonical_url" value="" />
</div>
<div class="form-field rx-term-field">
<label for="rx_article_level_hero_image_id"><?php esc_html_e( 'Hero Image ID', 'rx-theme' ); ?></label>
<input type="number" name="rx_article_level_meta[hero_image_id]" id="rx_article_level_hero_image_id" value="0" min="0" step="1" />
</div>
<?php
}
/**
* Edit term fields.
*
* @param WP_Term $term Term object.
* @return void
*/
public static function edit_term_fields( $term ) {
wp_nonce_field( 'rx_article_level_save_meta', 'rx_article_level_nonce' );
$meta = self::get_term_meta_array( $term->term_id );
?>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_color"><?php esc_html_e( 'Text Color', 'rx-theme' ); ?></label></th>
<td>
<input type="text" name="rx_article_level_meta[color]" id="rx_article_level_color" value="<?php echo esc_attr( $meta['color'] ); ?>" class="rx-color-field" />
<p class="description"><?php esc_html_e( 'Badge text color.', 'rx-theme' ); ?></p>
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_background_color"><?php esc_html_e( 'Background Color', 'rx-theme' ); ?></label></th>
<td>
<input type="text" name="rx_article_level_meta[background_color]" id="rx_article_level_background_color" value="<?php echo esc_attr( $meta['background_color'] ); ?>" class="rx-color-field" />
<p class="description"><?php esc_html_e( 'Badge background color.', 'rx-theme' ); ?></p>
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_icon"><?php esc_html_e( 'Icon', 'rx-theme' ); ?></label></th>
<td>
<input type="text" name="rx_article_level_meta[icon]" id="rx_article_level_icon" value="<?php echo esc_attr( $meta['icon'] ); ?>" />
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_order"><?php esc_html_e( 'Display Order', 'rx-theme' ); ?></label></th>
<td>
<input type="number" name="rx_article_level_meta[order]" id="rx_article_level_order" value="<?php echo esc_attr( $meta['order'] ); ?>" min="0" step="1" />
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_reading_age"><?php esc_html_e( 'Reading Age / Audience', 'rx-theme' ); ?></label></th>
<td>
<input type="text" name="rx_article_level_meta[reading_age]" id="rx_article_level_reading_age" value="<?php echo esc_attr( $meta['reading_age'] ); ?>" />
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_difficulty_score"><?php esc_html_e( 'Difficulty Score', 'rx-theme' ); ?></label></th>
<td>
<input type="number" name="rx_article_level_meta[difficulty_score]" id="rx_article_level_difficulty_score" value="<?php echo esc_attr( $meta['difficulty_score'] ); ?>" min="1" max="10" step="1" />
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_estimated_minutes"><?php esc_html_e( 'Estimated Minutes', 'rx-theme' ); ?></label></th>
<td>
<input type="number" name="rx_article_level_meta[estimated_minutes]" id="rx_article_level_estimated_minutes" value="<?php echo esc_attr( $meta['estimated_minutes'] ); ?>" min="0" step="1" />
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_seo_title"><?php esc_html_e( 'SEO Title', 'rx-theme' ); ?></label></th>
<td>
<input type="text" class="large-text" name="rx_article_level_meta[seo_title]" id="rx_article_level_seo_title" value="<?php echo esc_attr( $meta['seo_title'] ); ?>" />
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_seo_description"><?php esc_html_e( 'SEO Description', 'rx-theme' ); ?></label></th>
<td>
<textarea class="large-text" name="rx_article_level_meta[seo_description]" id="rx_article_level_seo_description" rows="4"><?php echo esc_textarea( $meta['seo_description'] ); ?></textarea>
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_schema_name"><?php esc_html_e( 'Schema Name', 'rx-theme' ); ?></label></th>
<td>
<input type="text" class="large-text" name="rx_article_level_meta[schema_name]" id="rx_article_level_schema_name" value="<?php echo esc_attr( $meta['schema_name'] ); ?>" />
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_schema_description"><?php esc_html_e( 'Schema Description', 'rx-theme' ); ?></label></th>
<td>
<textarea class="large-text" name="rx_article_level_meta[schema_description]" id="rx_article_level_schema_description" rows="4"><?php echo esc_textarea( $meta['schema_description'] ); ?></textarea>
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><?php esc_html_e( 'Featured', 'rx-theme' ); ?></th>
<td>
<label>
<input type="checkbox" name="rx_article_level_meta[featured]" value="1" <?php checked( $meta['featured'] ); ?> />
<?php esc_html_e( 'Feature this article level', 'rx-theme' ); ?>
</label>
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><?php esc_html_e( 'Noindex', 'rx-theme' ); ?></th>
<td>
<label>
<input type="checkbox" name="rx_article_level_meta[noindex]" value="1" <?php checked( $meta['noindex'] ); ?> />
<?php esc_html_e( 'Ask search engines not to index this archive', 'rx-theme' ); ?>
</label>
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_canonical_url"><?php esc_html_e( 'Canonical URL', 'rx-theme' ); ?></label></th>
<td>
<input type="url" class="large-text" name="rx_article_level_meta[canonical_url]" id="rx_article_level_canonical_url" value="<?php echo esc_url( $meta['canonical_url'] ); ?>" />
</td>
</tr>
<tr class="form-field rx-term-field">
<th scope="row"><label for="rx_article_level_hero_image_id"><?php esc_html_e( 'Hero Image ID', 'rx-theme' ); ?></label></th>
<td>
<input type="number" name="rx_article_level_meta[hero_image_id]" id="rx_article_level_hero_image_id" value="<?php echo esc_attr( $meta['hero_image_id'] ); ?>" min="0" step="1" />
<?php
if ( ! empty( $meta['hero_image_id'] ) ) {
echo '<div style="margin-top:10px;">';
echo wp_get_attachment_image( absint( $meta['hero_image_id'] ), 'thumbnail' );
echo '</div>';
}
?>
</td>
</tr>
<?php
}
/**
* Save term meta.
*
* @param int $term_id Term ID.
* @return void
*/
public static function save_term_meta( $term_id ) {
if ( ! isset( $_POST['rx_article_level_nonce'] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['rx_article_level_nonce'] ) ), 'rx_article_level_save_meta' ) ) {
return;
}
if ( ! current_user_can( 'manage_categories' ) ) {
return;
}
$posted = isset( $_POST['rx_article_level_meta'] ) && is_array( $_POST['rx_article_level_meta'] )
? wp_unslash( $_POST['rx_article_level_meta'] )
: array();
$fields = array(
'color' => array( 'callback' => array( __CLASS__, 'sanitize_color' ), 'default' => '#ffffff' ),
'background_color' => array( 'callback' => array( __CLASS__, 'sanitize_color' ), 'default' => '#0f766e' ),
'icon' => array( 'callback' => 'sanitize_text_field', 'default' => '' ),
'order' => array( 'callback' => 'absint', 'default' => 0 ),
'reading_age' => array( 'callback' => 'sanitize_text_field', 'default' => '' ),
'difficulty_score' => array( 'callback' => array( __CLASS__, 'sanitize_score' ), 'default' => 1 ),
'estimated_minutes' => array( 'callback' => 'absint', 'default' => 0 ),
'seo_title' => array( 'callback' => 'sanitize_text_field', 'default' => '' ),
'seo_description' => array( 'callback' => 'sanitize_textarea_field', 'default' => '' ),
'schema_name' => array( 'callback' => 'sanitize_text_field', 'default' => '' ),
'schema_description' => array( 'callback' => 'sanitize_textarea_field', 'default' => '' ),
'canonical_url' => array( 'callback' => 'esc_url_raw', 'default' => '' ),
'hero_image_id' => array( 'callback' => 'absint', 'default' => 0 ),
);
foreach ( $fields as $field => $config ) {
$value = isset( $posted[ $field ] ) ? $posted[ $field ] : $config['default'];
$value = call_user_func( $config['callback'], $value );
update_term_meta( $term_id, self::$meta_keys[ $field ], $value );
}
update_term_meta( $term_id, self::$meta_keys['featured'], ! empty( $posted['featured'] ) ? 1 : 0 );
update_term_meta( $term_id, self::$meta_keys['noindex'], ! empty( $posted['noindex'] ) ? 1 : 0 );
}
/**
* Admin assets.
*
* @param string $hook Hook name.
* @return void
*/
public static function admin_assets( $hook ) {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || self::TAXONOMY !== $screen->taxonomy ) {
return;
}
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
$script = "
jQuery(document).ready(function($){
$('.rx-color-field').wpColorPicker();
});
";
wp_add_inline_script( 'wp-color-picker', $script );
}
/**
* Admin columns.
*
* @param array $columns Columns.
* @return array
*/
public static function admin_columns( $columns ) {
$new = array();
foreach ( $columns as $key => $label ) {
$new[ $key ] = $label;
if ( 'name' === $key ) {
$new['rx_badge'] = __( 'Badge', 'rx-theme' );
$new['rx_score'] = __( 'Score', 'rx-theme' );
$new['rx_audience'] = __( 'Audience', 'rx-theme' );
$new['rx_featured'] = __( 'Featured', 'rx-theme' );
$new['rx_noindex'] = __( 'Noindex', 'rx-theme' );
$new['rx_order'] = __( 'Order', 'rx-theme' );
}
}
return $new;
}
/**
* Admin column content.
*
* @param string $content Content.
* @param string $column_name Column.
* @param int $term_id Term ID.
* @return string
*/
public static function admin_column_content( $content, $column_name, $term_id ) {
$meta = self::get_term_meta_array( $term_id );
switch ( $column_name ) {
case 'rx_badge':
return self::get_level_badge_html_by_term_id( $term_id, false );
case 'rx_score':
return esc_html( $meta['difficulty_score'] . '/10' );
case 'rx_audience':
return esc_html( $meta['reading_age'] );
case 'rx_featured':
return $meta['featured'] ? '✅' : '—';
case 'rx_noindex':
return $meta['noindex'] ? '🚫' : '—';
case 'rx_order':
return esc_html( $meta['order'] );
default:
return $content;
}
}
/**
* Sortable columns.
*
* @param array $columns Columns.
* @return array
*/
public static function sortable_columns( $columns ) {
$columns['rx_order'] = 'rx_order';
$columns['rx_score'] = 'rx_score';
return $columns;
}
/**
* Admin post filter dropdown.
*
* @return void
*/
public static function admin_filter_dropdown() {
global $typenow;
if ( empty( $typenow ) || ! in_array( $typenow, self::get_supported_post_types(), true ) ) {
return;
}
$selected = isset( $_GET[ self::TAXONOMY ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::TAXONOMY ] ) ) : '';
wp_dropdown_categories(
array(
'show_option_all' => __( 'All Article Levels', 'rx-theme' ),
'taxonomy' => self::TAXONOMY,
'name' => self::TAXONOMY,
'orderby' => 'name',
'selected' => $selected,
'hierarchical' => true,
'depth' => 3,
'show_count' => true,
'hide_empty' => false,
'value_field' => 'slug',
)
);
}
/**
* Admin filter query.
*
* @param WP_Query $query Query.
* @return WP_Query
*/
public static function admin_filter_query( $query ) {
global $pagenow;
if ( ! is_admin() || 'edit.php' !== $pagenow ) {
return $query;
}
if ( empty( $_GET[ self::TAXONOMY ] ) ) {
return $query;
}
$slug = sanitize_text_field( wp_unslash( $_GET[ self::TAXONOMY ] ) );
if ( '0' === $slug ) {
return $query;
}
$query->query_vars[ self::TAXONOMY ] = $slug;
return $query;
}
/**
* Archive title.
*
* @param string $title Archive title.
* @return string
*/
public static function archive_title( $title ) {
if ( is_tax( self::TAXONOMY ) ) {
$term = get_queried_object();
if ( $term instanceof WP_Term ) {
return sprintf(
/* translators: %s: taxonomy term name */
__( 'Article Level: %s', 'rx-theme' ),
single_term_title( '', false )
);
}
}
return $title;
}
/**
* Document title parts.
*
* @param array $parts Title parts.
* @return array
*/
public static function document_title_parts( $parts ) {
if ( ! is_tax( self::TAXONOMY ) ) {
return $parts;
}
$term = get_queried_object();
if ( ! $term instanceof WP_Term ) {
return $parts;
}
$seo_title = get_term_meta( $term->term_id, self::$meta_keys['seo_title'], true );
if ( $seo_title ) {
$parts['title'] = $seo_title;
}
return $parts;
}
/**
* Archive meta tags.
*
* @return void
*/
public static function archive_meta_tags() {
if ( ! is_tax( self::TAXONOMY ) ) {
return;
}
$term = get_queried_object();
if ( ! $term instanceof WP_Term ) {
return;
}
$description = get_term_meta( $term->term_id, self::$meta_keys['seo_description'], true );
$noindex = (bool) get_term_meta( $term->term_id, self::$meta_keys['noindex'], true );
$canonical = get_term_meta( $term->term_id, self::$meta_keys['canonical_url'], true );
if ( ! $description ) {
$description = term_description( $term, self::TAXONOMY );
$description = wp_strip_all_tags( $description );
}
if ( $description ) {
echo "\n" . '<meta name="description" content="' . esc_attr( wp_trim_words( $description, 28, '' ) ) . '">' . "\n";
}
if ( $noindex ) {
echo '<meta name="robots" content="noindex,follow">' . "\n";
}
if ( $canonical ) {
echo '<link rel="canonical" href="' . esc_url( $canonical ) . '">' . "\n";
} else {
$link = get_term_link( $term );
if ( ! is_wp_error( $link ) ) {
echo '<link rel="canonical" href="' . esc_url( $link ) . '">' . "\n";
}
}
}
/**
* Schema JSON-LD.
*
* @return void
*/
public static function schema_json_ld() {
if ( is_tax( self::TAXONOMY ) ) {
self::taxonomy_archive_schema();
return;
}
if ( is_singular( self::get_supported_post_types() ) ) {
self::single_article_level_schema();
}
}
/**
* Taxonomy archive schema.
*
* @return void
*/
private static function taxonomy_archive_schema() {
$term = get_queried_object();
if ( ! $term instanceof WP_Term ) {
return;
}
$link = get_term_link( $term );
if ( is_wp_error( $link ) ) {
return;
}
$schema_name = get_term_meta( $term->term_id, self::$meta_keys['schema_name'], true );
$schema_desc = get_term_meta( $term->term_id, self::$meta_keys['schema_description'], true );
$data = array(
'@context' => 'https://schema.org',
'@type' => 'CollectionPage',
'name' => $schema_name ? $schema_name : $term->name,
'description' => $schema_desc ? $schema_desc : wp_strip_all_tags( term_description( $term, self::TAXONOMY ) ),
'url' => esc_url_raw( $link ),
'isPartOf' => array(
'@type' => 'WebSite',
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
),
'about' => array(
'@type' => 'DefinedTerm',
'name' => $term->name,
'url' => esc_url_raw( $link ),
),
);
self::print_json_ld( $data );
}
/**
* Single post schema addition.
*
* @return void
*/
private static function single_article_level_schema() {
$post_id = get_queried_object_id();
if ( ! $post_id ) {
return;
}
$terms = get_the_terms( $post_id, self::TAXONOMY );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return;
}
$level_items = array();
foreach ( $terms as $term ) {
$link = get_term_link( $term );
if ( is_wp_error( $link ) ) {
continue;
}
$meta = self::get_term_meta_array( $term->term_id );
$level_items[] = array(
'@type' => 'DefinedTerm',
'name' => $term->name,
'description' => wp_strip_all_tags( $term->description ),
'url' => esc_url_raw( $link ),
'termCode' => sanitize_title( $term->slug ),
'inDefinedTermSet' => array(
'@type' => 'DefinedTermSet',
'name' => 'Article Level',
),
'additionalProperty' => array(
array(
'@type' => 'PropertyValue',
'name' => 'difficultyScore',
'value' => absint( $meta['difficulty_score'] ),
),
array(
'@type' => 'PropertyValue',
'name' => 'audience',
'value' => $meta['reading_age'],
),
),
);
}
if ( empty( $level_items ) ) {
return;
}
$data = array(
'@context' => 'https://schema.org',
'@type' => 'WebPage',
'@id' => get_permalink( $post_id ) . '#article-level',
'name' => get_the_title( $post_id ),
'url' => get_permalink( $post_id ),
'educationalLevel' => wp_list_pluck( $terms, 'name' ),
'about' => $level_items,
);
self::print_json_ld( $data );
}
/**
* Print JSON-LD.
*
* @param array $data Schema data.
* @return void
*/
private static function print_json_ld( $data ) {
$data = apply_filters( 'rx_article_level_schema_data', $data );
if ( empty( $data ) || ! is_array( $data ) ) {
return;
}
echo "\n" . '<script type="application/ld+json">' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>' . "\n";
}
/**
* Add body classes.
*
* @param array $classes Classes.
* @return array
*/
public static function body_classes( $classes ) {
if ( is_tax( self::TAXONOMY ) ) {
$term = get_queried_object();
if ( $term instanceof WP_Term ) {
$classes[] = 'rx-article-level-archive';
$classes[] = 'rx-article-level-' . sanitize_html_class( $term->slug );
}
}
if ( is_singular( self::get_supported_post_types() ) ) {
$terms = get_the_terms( get_queried_object_id(), self::TAXONOMY );
if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
foreach ( $terms as $term ) {
$classes[] = 'rx-has-article-level-' . sanitize_html_class( $term->slug );
}
}
}
return array_unique( $classes );
}
/**
* Add post classes.
*
* @param array $classes Classes.
* @param string $class Extra class.
* @param int|false $post_id Post ID.
* @return array
*/
public static function post_classes( $classes, $class, $post_id ) {
if ( ! $post_id ) {
return $classes;
}
$terms = get_the_terms( $post_id, self::TAXONOMY );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return $classes;
}
$classes[] = 'rx-has-article-level';
foreach ( $terms as $term ) {
$classes[] = 'rx-article-level-' . sanitize_html_class( $term->slug );
}
return array_unique( $classes );
}
/**
* Shortcode: [rx_article_level]
*
* @param array $atts Attributes.
* @return string
*/
public static function shortcode_article_level( $atts ) {
$atts = shortcode_atts(
array(
'post_id' => get_the_ID(),
'link' => 'yes',
'show_icon' => 'yes',
'show_score' => 'no',
'class' => '',
'empty' => '',
),
$atts,
'rx_article_level'
);
$post_id = absint( $atts['post_id'] );
if ( ! $post_id ) {
return esc_html( $atts['empty'] );
}
$html = self::get_post_level_badges(
$post_id,
array(
'link' => 'yes' === strtolower( $atts['link'] ),
'show_icon' => 'yes' === strtolower( $atts['show_icon'] ),
'show_score' => 'yes' === strtolower( $atts['show_score'] ),
'class' => sanitize_html_class( $atts['class'] ),
)
);
return $html ? $html : esc_html( $atts['empty'] );
}
/**
* Shortcode: [rx_article_level_list]
*
* @param array $atts Attributes.
* @return string
*/
public static function shortcode_article_level_list( $atts ) {
$atts = shortcode_atts(
array(
'featured' => '',
'hide_empty' => 'no',
'show_count' => 'yes',
'show_desc' => 'yes',
'orderby' => 'meta_order',
'limit' => 0,
'class' => '',
),
$atts,
'rx_article_level_list'
);
$args = array(
'taxonomy' => self::TAXONOMY,
'hide_empty' => 'yes' === strtolower( $atts['hide_empty'] ),
);
$terms = get_terms( $args );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return '';
}
$terms = self::sort_terms( $terms );
if ( '' !== $atts['featured'] ) {
$want_featured = 'yes' === strtolower( $atts['featured'] );
$terms = array_filter(
$terms,
function( $term ) use ( $want_featured ) {
return (bool) get_term_meta( $term->term_id, self::$meta_keys['featured'], true ) === $want_featured;
}
);
}
$limit = absint( $atts['limit'] );
if ( $limit > 0 ) {
$terms = array_slice( $terms, 0, $limit );
}
$class = 'rx-article-level-list';
if ( $atts['class'] ) {
$class .= ' ' . sanitize_html_class( $atts['class'] );
}
ob_start();
?>
<div class="<?php echo esc_attr( $class ); ?>">
<?php foreach ( $terms as $term ) : ?>
<?php
$link = get_term_link( $term );
if ( is_wp_error( $link ) ) {
continue;
}
?>
<div class="rx-article-level-list__item">
<?php echo self::get_level_badge_html_by_term_id( $term->term_id, true ); ?>
<?php if ( 'yes' === strtolower( $atts['show_count'] ) ) : ?>
<span class="rx-article-level-list__count">
<?php echo esc_html( number_format_i18n( $term->count ) ); ?>
</span>
<?php endif; ?>
<?php if ( 'yes' === strtolower( $atts['show_desc'] ) && $term->description ) : ?>
<p class="rx-article-level-list__description">
<?php echo esc_html( wp_trim_words( $term->description, 22 ) ); ?>
</p>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Maybe assign default level to posts.
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @param bool $update Is update.
* @return void
*/
public static function maybe_assign_default_level( $post_id, $post, $update ) {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
return;
}
if ( empty( $post->post_type ) || ! in_array( $post->post_type, self::get_supported_post_types(), true ) ) {
return;
}
if ( 'auto-draft' === $post->post_status ) {
return;
}
$enabled = apply_filters( 'rx_article_level_auto_assign_default', false, $post_id, $post );
if ( ! $enabled ) {
return;
}
$terms = wp_get_object_terms( $post_id, self::TAXONOMY, array( 'fields' => 'ids' ) );
if ( ! empty( $terms ) || is_wp_error( $terms ) ) {
return;
}
$default = get_term_by( 'slug', 'general-reader', self::TAXONOMY );
if ( $default instanceof WP_Term ) {
wp_set_object_terms( $post_id, array( $default->term_id ), self::TAXONOMY, false );
}
}
/**
* Get term meta array.
*
* @param int $term_id Term ID.
* @return array
*/
public static function get_term_meta_array( $term_id ) {
$data = array(
'color' => get_term_meta( $term_id, self::$meta_keys['color'], true ),
'background_color' => get_term_meta( $term_id, self::$meta_keys['background_color'], true ),
'icon' => get_term_meta( $term_id, self::$meta_keys['icon'], true ),
'order' => get_term_meta( $term_id, self::$meta_keys['order'], true ),
'reading_age' => get_term_meta( $term_id, self::$meta_keys['reading_age'], true ),
'difficulty_score' => get_term_meta( $term_id, self::$meta_keys['difficulty_score'], true ),
'estimated_minutes' => get_term_meta( $term_id, self::$meta_keys['estimated_minutes'], true ),
'seo_title' => get_term_meta( $term_id, self::$meta_keys['seo_title'], true ),
'seo_description' => get_term_meta( $term_id, self::$meta_keys['seo_description'], true ),
'schema_name' => get_term_meta( $term_id, self::$meta_keys['schema_name'], true ),
'schema_description' => get_term_meta( $term_id, self::$meta_keys['schema_description'], true ),
'featured' => (bool) get_term_meta( $term_id, self::$meta_keys['featured'], true ),
'noindex' => (bool) get_term_meta( $term_id, self::$meta_keys['noindex'], true ),
'canonical_url' => get_term_meta( $term_id, self::$meta_keys['canonical_url'], true ),
'hero_image_id' => absint( get_term_meta( $term_id, self::$meta_keys['hero_image_id'], true ) ),
);
$defaults = array(
'color' => '#ffffff',
'background_color' => '#0f766e',
'icon' => '',
'order' => 0,
'reading_age' => '',
'difficulty_score' => 1,
'estimated_minutes' => 0,
'seo_title' => '',
'seo_description' => '',
'schema_name' => '',
'schema_description' => '',
'featured' => false,
'noindex' => false,
'canonical_url' => '',
'hero_image_id' => 0,
);
$data = wp_parse_args( $data, $defaults );
$data['color'] = self::sanitize_color( $data['color'] );
$data['background_color'] = self::sanitize_color( $data['background_color'] );
$data['order'] = absint( $data['order'] );
$data['difficulty_score'] = self::sanitize_score( $data['difficulty_score'] );
$data['estimated_minutes'] = absint( $data['estimated_minutes'] );
return apply_filters( 'rx_article_level_term_meta_array', $data, $term_id );
}
/**
* Get post level badges.
*
* @param int $post_id Post ID.
* @param array $args Args.
* @return string
*/
public static function get_post_level_badges( $post_id, $args = array() ) {
$defaults = array(
'link' => true,
'show_icon' => true,
'show_score' => false,
'class' => '',
);
$args = wp_parse_args( $args, $defaults );
$terms = get_the_terms( $post_id, self::TAXONOMY );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return '';
}
$terms = self::sort_terms( $terms );
$html = '<span class="rx-article-level-badges">';
foreach ( $terms as $term ) {
$html .= self::get_level_badge_html( $term, $args );
}
$html .= '</span>';
return apply_filters( 'rx_article_level_post_badges_html', $html, $post_id, $terms, $args );
}
/**
* Get badge by term ID.
*
* @param int $term_id Term ID.
* @param bool $link Whether to link.
* @return string
*/
public static function get_level_badge_html_by_term_id( $term_id, $link = true ) {
$term = get_term( $term_id, self::TAXONOMY );
if ( ! $term instanceof WP_Term || is_wp_error( $term ) ) {
return '';
}
return self::get_level_badge_html(
$term,
array(
'link' => $link,
'show_icon' => true,
'show_score' => false,
'class' => '',
)
);
}
/**
* Get badge HTML.
*
* @param WP_Term $term Term object.
* @param array $args Args.
* @return string
*/
public static function get_level_badge_html( $term, $args = array() ) {
if ( ! $term instanceof WP_Term ) {
return '';
}
$defaults = array(
'link' => true,
'show_icon' => true,
'show_score' => false,
'class' => '',
);
$args = wp_parse_args( $args, $defaults );
$meta = self::get_term_meta_array( $term->term_id );
$style = sprintf(
'color:%s;background-color:%s;',
esc_attr( $meta['color'] ),
esc_attr( $meta['background_color'] )
);
$class = 'rx-article-level-badge rx-article-level-badge--' . sanitize_html_class( $term->slug );
if ( $args['class'] ) {
$class .= ' ' . sanitize_html_class( $args['class'] );
}
$label = '';
if ( $args['show_icon'] && $meta['icon'] ) {
$label .= '<span class="rx-article-level-badge__icon" aria-hidden="true">' . esc_html( $meta['icon'] ) . '</span> ';
}
$label .= '<span class="rx-article-level-badge__name">' . esc_html( $term->name ) . '</span>';
if ( $args['show_score'] ) {
$label .= ' <span class="rx-article-level-badge__score">' . esc_html( $meta['difficulty_score'] ) . '/10</span>';
}
$title = $meta['reading_age']
? sprintf( '%s - %s', $term->name, $meta['reading_age'] )
: $term->name;
if ( $args['link'] ) {
$link = get_term_link( $term );
if ( ! is_wp_error( $link ) ) {
return sprintf(
'<a class="%1$s" href="%2$s" style="%3$s" title="%4$s">%5$s</a>',
esc_attr( $class ),
esc_url( $link ),
esc_attr( $style ),
esc_attr( $title ),
$label
);
}
}
return sprintf(
'<span class="%1$s" style="%2$s" title="%3$s">%4$s</span>',
esc_attr( $class ),
esc_attr( $style ),
esc_attr( $title ),
$label
);
}
/**
* Sort terms by custom order then name.
*
* @param array $terms Terms.
* @return array
*/
public static function sort_terms( $terms ) {
if ( empty( $terms ) || ! is_array( $terms ) ) {
return array();
}
usort(
$terms,
function( $a, $b ) {
$a_order = absint( get_term_meta( $a->term_id, self::$meta_keys['order'], true ) );
$b_order = absint( get_term_meta( $b->term_id, self::$meta_keys['order'], true ) );
if ( $a_order === $b_order ) {
return strcasecmp( $a->name, $b->name );
}
return $a_order <=> $b_order;
}
);
return $terms;
}
/**
* Term link filter.
*
* @param string $termlink Term link.
* @param WP_Term $term Term.
* @param string $taxonomy Taxonomy.
* @return string
*/
public static function term_link_filter( $termlink, $term, $taxonomy ) {
if ( self::TAXONOMY !== $taxonomy ) {
return $termlink;
}
return apply_filters( 'rx_article_level_term_link', $termlink, $term, $taxonomy );
}
/**
* Sanitize color.
*
* @param mixed $color Color.
* @return string
*/
public static function sanitize_color( $color ) {
$color = sanitize_text_field( (string) $color );
if ( '' === $color ) {
return '';
}
if ( function_exists( 'sanitize_hex_color' ) ) {
$hex = sanitize_hex_color( $color );
if ( $hex ) {
return $hex;
}
}
if ( preg_match( '/^#[a-fA-F0-9]{3}$/', $color ) || preg_match( '/^#[a-fA-F0-9]{6}$/', $color ) ) {
return $color;
}
return '#0f766e';
}
/**
* Sanitize difficulty score.
*
* @param mixed $score Score.
* @return int
*/
public static function sanitize_score( $score ) {
$score = absint( $score );
if ( $score < 1 ) {
$score = 1;
}
if ( $score > 10 ) {
$score = 10;
}
return $score;
}
/**
* Sanitize bool.
*
* @param mixed $value Value.
* @return bool
*/
public static function sanitize_bool( $value ) {
return (bool) $value;
}
/**
* Flush rewrites after switching theme.
*
* @return void
*/
public static function flush_rewrite_rules_on_theme_switch() {
self::register_taxonomy();
flush_rewrite_rules();
}
}
RX_Article_Level_Taxonomy::boot();
endif;
/**
* Helper: Get article level taxonomy key.
*
* @return string
*/
function rx_article_level_taxonomy() {
return RX_Article_Level_Taxonomy::TAXONOMY;
}
/**
* Helper: Get article level terms for a post.
*
* @param int|null $post_id Post ID.
* @return array
*/
function rx_get_article_levels( $post_id = null ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id ) {
return array();
}
$terms = get_the_terms( $post_id, RX_Article_Level_Taxonomy::TAXONOMY );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
return array();
}
return RX_Article_Level_Taxonomy::sort_terms( $terms );
}
/**
* Helper: Get article level badges HTML.
*
* @param int|null $post_id Post ID.
* @param array $args Args.
* @return string
*/
function rx_get_article_level_badges( $post_id = null, $args = array() ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id ) {
return '';
}
return RX_Article_Level_Taxonomy::get_post_level_badges( $post_id, $args );
}
/**
* Helper: Echo article level badges.
*
* @param int|null $post_id Post ID.
* @param array $args Args.
* @return void
*/
function rx_the_article_level_badges( $post_id = null, $args = array() ) {
echo wp_kses_post( rx_get_article_level_badges( $post_id, $args ) );
}
/**
* Helper: Get first article level.
*
* @param int|null $post_id Post ID.
* @return WP_Term|null
*/
function rx_get_primary_article_level( $post_id = null ) {
$levels = rx_get_article_levels( $post_id );
if ( empty( $levels ) ) {
return null;
}
return reset( $levels );
}
/**
* Helper: Check whether post has article level.
*
* @param string|int $level Term slug, name, or ID.
* @param int|null $post_id Post ID.
* @return bool
*/
function rx_has_article_level( $level, $post_id = null ) {
$post_id = $post_id ? absint( $post_id ) : get_the_ID();
if ( ! $post_id || empty( $level ) ) {
return false;
}
return has_term( $level, RX_Article_Level_Taxonomy::TAXONOMY, $post_id );
}
Now include it in your theme functions.php:
/**
* RX Theme Taxonomies.
*/
require_once get_template_directory() . '/inc/taxonomies/article-level.php';
Optional frontend CSS for style.css:
.rx-article-level-badges {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.rx-article-level-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 999px;
font-size: 13px;
font-weight: 700;
line-height: 1.2;
text-decoration: none;
vertical-align: middle;
}
.rx-article-level-badge:hover,
.rx-article-level-badge:focus {
opacity: 0.88;
text-decoration: none;
}
.rx-article-level-badge__icon {
font-size: 14px;
line-height: 1;
}
.rx-article-level-badge__score {
font-size: 12px;
opacity: 0.9;
}
.rx-article-level-list {
display: grid;
gap: 14px;
margin: 20px 0;
}
.rx-article-level-list__item {
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 12px;
background: #ffffff;
}
.rx-article-level-list__count {
margin-left: 8px;
font-size: 13px;
font-weight: 700;
color: #4b5563;
}
.rx-article-level-list__description {
margin: 10px 0 0;
color: #4b5563;
}
Usage in template:
<?php rx_the_article_level_badges(); ?>
Shortcodes:
[rx_article_level]
[rx_article_level show_score="yes"]
[rx_article_level_list show_count="yes" show_desc="yes"]
[rx_article_level_list featured="yes"]
After adding this file, go to:
WordPress Dashboard → Settings → Permalinks → Save Changes
This refreshes rewrite rules for the new /article-level/ archive URLs.