I’ll build this as a complete advanced recent-posts-widget.php file for your RX theme: secure widget class, many display options, caching, thumbnails, meta, taxonomy filters, ordering, schema-friendly markup, and safe output.
Below is an advanced inc/widgets/recent-posts-widget.php file for your RX Theme.
You can replace your current file with this code.
<?php
/**
* RX Theme - Advanced Recent Posts Widget
*
* File: inc/widgets/recent-posts-widget.php
*
* Features:
* - Recent / popular / random / modified / commented posts
* - Post type support
* - Category, tag, taxonomy filters
* - Include / exclude post IDs
* - Author filter
* - Date range filter
* - Thumbnail support
* - Meta display: date, author, comments, categories, tags, reading time
* - Excerpt support
* - Custom title length
* - Custom excerpt length
* - Sticky posts option
* - Query order options
* - Offset support
* - Pagination-like “View All” link
* - Schema-friendly HTML
* - Transient caching
* - Object cache friendly
* - Accessibility-friendly markup
* - RTL-safe / theme-friendly classes
* - Elementor/block-theme compatible sidebar widget
*
* @package RX_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'RX_Advanced_Recent_Posts_Widget' ) ) :
class RX_Advanced_Recent_Posts_Widget extends WP_Widget {
/**
* Widget defaults.
*/
protected $defaults = array(
'title' => 'Recent Posts',
'post_type' => 'post',
'posts_per_page' => 5,
'offset' => 0,
'orderby' => 'date',
'order' => 'DESC',
'category_ids' => '',
'tag_ids' => '',
'taxonomy' => '',
'taxonomy_terms' => '',
'author_ids' => '',
'include_posts' => '',
'exclude_posts' => '',
'date_after' => '',
'date_before' => '',
'show_thumbnail' => 1,
'thumbnail_size' => 'thumbnail',
'thumbnail_width' => 90,
'thumbnail_height' => 90,
'thumbnail_position' => 'left',
'fallback_image' => '',
'show_title' => 1,
'title_words' => 12,
'show_excerpt' => 0,
'excerpt_words' => 18,
'show_date' => 1,
'show_modified_date' => 0,
'show_author' => 0,
'show_comments' => 0,
'show_category' => 0,
'show_tags' => 0,
'show_reading_time' => 0,
'show_view_all' => 0,
'view_all_text' => 'View All Posts',
'view_all_url' => '',
'open_new_tab' => 0,
'nofollow_links' => 0,
'ignore_sticky' => 1,
'only_sticky' => 0,
'hide_current_post' => 1,
'enable_cache' => 1,
'cache_time' => 30,
'layout_style' => 'list',
'wrapper_class' => '',
'item_class' => '',
'before_item' => '',
'after_item' => '',
);
/**
* Constructor.
*/
public function __construct() {
parent::__construct(
'rx_advanced_recent_posts_widget',
esc_html__( 'RX Advanced Recent Posts', 'rx-theme' ),
array(
'classname' => 'rx-advanced-recent-posts-widget',
'description' => esc_html__( 'Powerful recent posts widget with thumbnail, meta, taxonomy filter, cache, and layout options.', 'rx-theme' ),
'customize_selective_refresh' => true,
)
);
add_action( 'save_post', array( $this, 'flush_widget_cache' ) );
add_action( 'deleted_post', array( $this, 'flush_widget_cache' ) );
add_action( 'switch_theme', array( $this, 'flush_widget_cache' ) );
}
/**
* Front-end widget output.
*/
public function widget( $args, $instance ) {
$instance = wp_parse_args( (array) $instance, $this->defaults );
$cache_key = $this->get_cache_key( $args, $instance );
if ( ! empty( $instance['enable_cache'] ) ) {
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
echo $cached; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
}
ob_start();
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters(
'widget_title',
! empty( $instance['title'] ) ? $instance['title'] : '',
$instance,
$this->id_base
);
if ( ! empty( $title ) ) {
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
$query_args = $this->build_query_args( $instance );
$recent_query = new WP_Query( $query_args );
$wrapper_classes = array(
'rx-recent-posts',
'rx-recent-posts-layout-' . sanitize_html_class( $instance['layout_style'] ),
'rx-thumb-' . sanitize_html_class( $instance['thumbnail_position'] ),
);
if ( ! empty( $instance['wrapper_class'] ) ) {
$wrapper_classes[] = sanitize_html_class( $instance['wrapper_class'] );
}
if ( $recent_query->have_posts() ) :
?>
<div class="<?php echo esc_attr( implode( ' ', array_filter( $wrapper_classes ) ) ); ?>">
<ul class="rx-recent-posts-list" itemscope itemtype="https://schema.org/ItemList">
<?php
$count = 0;
while ( $recent_query->have_posts() ) :
$recent_query->the_post();
$count++;
$post_id = get_the_ID();
$item_classes = array(
'rx-recent-post-item',
'rx-recent-post-item-' . absint( $count ),
'clearfix',
);
if ( has_post_thumbnail( $post_id ) ) {
$item_classes[] = 'has-post-thumbnail';
} else {
$item_classes[] = 'no-post-thumbnail';
}
if ( ! empty( $instance['item_class'] ) ) {
$item_classes[] = sanitize_html_class( $instance['item_class'] );
}
$link_attrs = $this->get_link_attrs( $instance );
?>
<li class="<?php echo esc_attr( implode( ' ', array_filter( $item_classes ) ) ); ?>" itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<meta itemprop="position" content="<?php echo esc_attr( $count ); ?>">
<?php
if ( ! empty( $instance['before_item'] ) ) {
echo wp_kses_post( $instance['before_item'] );
}
?>
<article class="rx-recent-post-article" itemscope itemtype="https://schema.org/BlogPosting">
<?php if ( ! empty( $instance['show_thumbnail'] ) ) : ?>
<?php echo $this->get_thumbnail_html( $post_id, $instance, $link_attrs ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php endif; ?>
<div class="rx-recent-post-content">
<?php if ( ! empty( $instance['show_title'] ) ) : ?>
<h4 class="rx-recent-post-title" itemprop="headline">
<a href="<?php echo esc_url( get_permalink( $post_id ) ); ?>" <?php echo $link_attrs; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> itemprop="url">
<?php echo esc_html( $this->trim_words( get_the_title( $post_id ), absint( $instance['title_words'] ) ) ); ?>
</a>
</h4>
<?php endif; ?>
<?php echo $this->get_meta_html( $post_id, $instance ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php if ( ! empty( $instance['show_excerpt'] ) ) : ?>
<div class="rx-recent-post-excerpt" itemprop="description">
<?php echo esc_html( $this->get_excerpt( $post_id, absint( $instance['excerpt_words'] ) ) ); ?>
</div>
<?php endif; ?>
</div>
</article>
<?php
if ( ! empty( $instance['after_item'] ) ) {
echo wp_kses_post( $instance['after_item'] );
}
?>
</li>
<?php endwhile; ?>
</ul>
<?php if ( ! empty( $instance['show_view_all'] ) ) : ?>
<?php
$view_all_url = ! empty( $instance['view_all_url'] ) ? $instance['view_all_url'] : get_post_type_archive_link( $instance['post_type'] );
if ( empty( $view_all_url ) && 'post' === $instance['post_type'] ) {
$view_all_url = get_permalink( get_option( 'page_for_posts' ) );
}
?>
<?php if ( ! empty( $view_all_url ) ) : ?>
<div class="rx-recent-posts-view-all">
<a class="rx-recent-posts-view-all-link" href="<?php echo esc_url( $view_all_url ); ?>" <?php echo $this->get_link_attrs( $instance ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php echo esc_html( $instance['view_all_text'] ); ?>
</a>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<?php
else :
?>
<p class="rx-recent-posts-empty">
<?php esc_html_e( 'No posts found.', 'rx-theme' ); ?>
</p>
<?php
endif;
wp_reset_postdata();
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$output = ob_get_clean();
if ( ! empty( $instance['enable_cache'] ) ) {
$cache_minutes = max( 1, absint( $instance['cache_time'] ) );
set_transient( $cache_key, $output, $cache_minutes * MINUTE_IN_SECONDS );
}
echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* Admin form.
*/
public function form( $instance ) {
$instance = wp_parse_args( (array) $instance, $this->defaults );
$post_types = get_post_types(
array(
'public' => true,
),
'objects'
);
$thumbnail_sizes = get_intermediate_image_sizes();
$orderby_options = array(
'date' => esc_html__( 'Published Date', 'rx-theme' ),
'modified' => esc_html__( 'Modified Date', 'rx-theme' ),
'title' => esc_html__( 'Title', 'rx-theme' ),
'comment_count' => esc_html__( 'Comment Count', 'rx-theme' ),
'rand' => esc_html__( 'Random', 'rx-theme' ),
'menu_order' => esc_html__( 'Menu Order', 'rx-theme' ),
'ID' => esc_html__( 'Post ID', 'rx-theme' ),
);
$layout_options = array(
'list' => esc_html__( 'List', 'rx-theme' ),
'card' => esc_html__( 'Card', 'rx-theme' ),
'compact' => esc_html__( 'Compact', 'rx-theme' ),
'grid' => esc_html__( 'Grid', 'rx-theme' ),
'minimal' => esc_html__( 'Minimal', 'rx-theme' ),
);
$thumb_positions = array(
'left' => esc_html__( 'Left', 'rx-theme' ),
'right' => esc_html__( 'Right', 'rx-theme' ),
'top' => esc_html__( 'Top', 'rx-theme' ),
'none' => esc_html__( 'None', 'rx-theme' ),
);
?>
<div class="rx-widget-admin rx-recent-posts-widget-admin">
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
<?php esc_html_e( 'Widget Title:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
type="text"
value="<?php echo esc_attr( $instance['title'] ); ?>">
</p>
<hr>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'post_type' ) ); ?>">
<?php esc_html_e( 'Post Type:', 'rx-theme' ); ?>
</label>
<select class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'post_type' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'post_type' ) ); ?>">
<?php foreach ( $post_types as $post_type ) : ?>
<option value="<?php echo esc_attr( $post_type->name ); ?>" <?php selected( $instance['post_type'], $post_type->name ); ?>>
<?php echo esc_html( $post_type->labels->singular_name ); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'posts_per_page' ) ); ?>">
<?php esc_html_e( 'Number of Posts:', 'rx-theme' ); ?>
</label>
<input class="tiny-text"
id="<?php echo esc_attr( $this->get_field_id( 'posts_per_page' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'posts_per_page' ) ); ?>"
type="number"
min="1"
max="50"
value="<?php echo esc_attr( absint( $instance['posts_per_page'] ) ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'offset' ) ); ?>">
<?php esc_html_e( 'Offset:', 'rx-theme' ); ?>
</label>
<input class="tiny-text"
id="<?php echo esc_attr( $this->get_field_id( 'offset' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'offset' ) ); ?>"
type="number"
min="0"
value="<?php echo esc_attr( absint( $instance['offset'] ) ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'orderby' ) ); ?>">
<?php esc_html_e( 'Order By:', 'rx-theme' ); ?>
</label>
<select class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'orderby' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'orderby' ) ); ?>">
<?php foreach ( $orderby_options as $key => $label ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $instance['orderby'], $key ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'order' ) ); ?>">
<?php esc_html_e( 'Order:', 'rx-theme' ); ?>
</label>
<select class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'order' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'order' ) ); ?>">
<option value="DESC" <?php selected( $instance['order'], 'DESC' ); ?>><?php esc_html_e( 'Descending', 'rx-theme' ); ?></option>
<option value="ASC" <?php selected( $instance['order'], 'ASC' ); ?>><?php esc_html_e( 'Ascending', 'rx-theme' ); ?></option>
</select>
</p>
<hr>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'category_ids' ) ); ?>">
<?php esc_html_e( 'Category IDs:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'category_ids' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'category_ids' ) ); ?>"
type="text"
placeholder="1,2,3"
value="<?php echo esc_attr( $instance['category_ids'] ); ?>">
<small><?php esc_html_e( 'Comma separated category IDs.', 'rx-theme' ); ?></small>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'tag_ids' ) ); ?>">
<?php esc_html_e( 'Tag IDs:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'tag_ids' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'tag_ids' ) ); ?>"
type="text"
placeholder="4,5,6"
value="<?php echo esc_attr( $instance['tag_ids'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'taxonomy' ) ); ?>">
<?php esc_html_e( 'Custom Taxonomy Slug:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'taxonomy' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'taxonomy' ) ); ?>"
type="text"
placeholder="medical_category"
value="<?php echo esc_attr( $instance['taxonomy'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'taxonomy_terms' ) ); ?>">
<?php esc_html_e( 'Custom Taxonomy Term IDs:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'taxonomy_terms' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'taxonomy_terms' ) ); ?>"
type="text"
placeholder="10,11,12"
value="<?php echo esc_attr( $instance['taxonomy_terms'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'author_ids' ) ); ?>">
<?php esc_html_e( 'Author IDs:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'author_ids' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'author_ids' ) ); ?>"
type="text"
placeholder="1,2"
value="<?php echo esc_attr( $instance['author_ids'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'include_posts' ) ); ?>">
<?php esc_html_e( 'Include Post IDs Only:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'include_posts' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'include_posts' ) ); ?>"
type="text"
placeholder="100,101,102"
value="<?php echo esc_attr( $instance['include_posts'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'exclude_posts' ) ); ?>">
<?php esc_html_e( 'Exclude Post IDs:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'exclude_posts' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'exclude_posts' ) ); ?>"
type="text"
placeholder="200,201"
value="<?php echo esc_attr( $instance['exclude_posts'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'date_after' ) ); ?>">
<?php esc_html_e( 'Date After:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'date_after' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'date_after' ) ); ?>"
type="date"
value="<?php echo esc_attr( $instance['date_after'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'date_before' ) ); ?>">
<?php esc_html_e( 'Date Before:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'date_before' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'date_before' ) ); ?>"
type="date"
value="<?php echo esc_attr( $instance['date_before'] ); ?>">
</p>
<hr>
<p>
<input class="checkbox"
id="<?php echo esc_attr( $this->get_field_id( 'show_thumbnail' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_thumbnail' ) ); ?>"
type="checkbox"
value="1" <?php checked( $instance['show_thumbnail'], 1 ); ?>>
<label for="<?php echo esc_attr( $this->get_field_id( 'show_thumbnail' ) ); ?>">
<?php esc_html_e( 'Show Thumbnail', 'rx-theme' ); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'thumbnail_size' ) ); ?>">
<?php esc_html_e( 'Thumbnail Size:', 'rx-theme' ); ?>
</label>
<select class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'thumbnail_size' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'thumbnail_size' ) ); ?>">
<?php foreach ( $thumbnail_sizes as $size ) : ?>
<option value="<?php echo esc_attr( $size ); ?>" <?php selected( $instance['thumbnail_size'], $size ); ?>>
<?php echo esc_html( $size ); ?>
</option>
<?php endforeach; ?>
<option value="full" <?php selected( $instance['thumbnail_size'], 'full' ); ?>>
<?php esc_html_e( 'Full', 'rx-theme' ); ?>
</option>
</select>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'thumbnail_position' ) ); ?>">
<?php esc_html_e( 'Thumbnail Position:', 'rx-theme' ); ?>
</label>
<select class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'thumbnail_position' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'thumbnail_position' ) ); ?>">
<?php foreach ( $thumb_positions as $key => $label ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $instance['thumbnail_position'], $key ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<p>
<label><?php esc_html_e( 'Thumbnail Width / Height:', 'rx-theme' ); ?></label><br>
<input class="small-text"
name="<?php echo esc_attr( $this->get_field_name( 'thumbnail_width' ) ); ?>"
type="number"
min="20"
value="<?php echo esc_attr( absint( $instance['thumbnail_width'] ) ); ?>">
×
<input class="small-text"
name="<?php echo esc_attr( $this->get_field_name( 'thumbnail_height' ) ); ?>"
type="number"
min="20"
value="<?php echo esc_attr( absint( $instance['thumbnail_height'] ) ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'fallback_image' ) ); ?>">
<?php esc_html_e( 'Fallback Image URL:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'fallback_image' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'fallback_image' ) ); ?>"
type="url"
value="<?php echo esc_url( $instance['fallback_image'] ); ?>">
</p>
<hr>
<p>
<input class="checkbox"
id="<?php echo esc_attr( $this->get_field_id( 'show_title' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_title' ) ); ?>"
type="checkbox"
value="1" <?php checked( $instance['show_title'], 1 ); ?>>
<label for="<?php echo esc_attr( $this->get_field_id( 'show_title' ) ); ?>">
<?php esc_html_e( 'Show Post Title', 'rx-theme' ); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'title_words' ) ); ?>">
<?php esc_html_e( 'Title Words:', 'rx-theme' ); ?>
</label>
<input class="tiny-text"
id="<?php echo esc_attr( $this->get_field_id( 'title_words' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'title_words' ) ); ?>"
type="number"
min="1"
max="50"
value="<?php echo esc_attr( absint( $instance['title_words'] ) ); ?>">
</p>
<p>
<input class="checkbox"
id="<?php echo esc_attr( $this->get_field_id( 'show_excerpt' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_excerpt' ) ); ?>"
type="checkbox"
value="1" <?php checked( $instance['show_excerpt'], 1 ); ?>>
<label for="<?php echo esc_attr( $this->get_field_id( 'show_excerpt' ) ); ?>">
<?php esc_html_e( 'Show Excerpt', 'rx-theme' ); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'excerpt_words' ) ); ?>">
<?php esc_html_e( 'Excerpt Words:', 'rx-theme' ); ?>
</label>
<input class="tiny-text"
id="<?php echo esc_attr( $this->get_field_id( 'excerpt_words' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'excerpt_words' ) ); ?>"
type="number"
min="1"
max="100"
value="<?php echo esc_attr( absint( $instance['excerpt_words'] ) ); ?>">
</p>
<hr>
<p><strong><?php esc_html_e( 'Meta Display Options', 'rx-theme' ); ?></strong></p>
<?php
$this->checkbox_field( $instance, 'show_date', esc_html__( 'Show Published Date', 'rx-theme' ) );
$this->checkbox_field( $instance, 'show_modified_date', esc_html__( 'Show Modified Date', 'rx-theme' ) );
$this->checkbox_field( $instance, 'show_author', esc_html__( 'Show Author', 'rx-theme' ) );
$this->checkbox_field( $instance, 'show_comments', esc_html__( 'Show Comment Count', 'rx-theme' ) );
$this->checkbox_field( $instance, 'show_category', esc_html__( 'Show Categories', 'rx-theme' ) );
$this->checkbox_field( $instance, 'show_tags', esc_html__( 'Show Tags', 'rx-theme' ) );
$this->checkbox_field( $instance, 'show_reading_time', esc_html__( 'Show Reading Time', 'rx-theme' ) );
?>
<hr>
<p><strong><?php esc_html_e( 'Advanced Options', 'rx-theme' ); ?></strong></p>
<?php
$this->checkbox_field( $instance, 'ignore_sticky', esc_html__( 'Ignore Sticky Posts', 'rx-theme' ) );
$this->checkbox_field( $instance, 'only_sticky', esc_html__( 'Show Only Sticky Posts', 'rx-theme' ) );
$this->checkbox_field( $instance, 'hide_current_post', esc_html__( 'Hide Current Post on Single Page', 'rx-theme' ) );
$this->checkbox_field( $instance, 'open_new_tab', esc_html__( 'Open Links in New Tab', 'rx-theme' ) );
$this->checkbox_field( $instance, 'nofollow_links', esc_html__( 'Add Nofollow to Links', 'rx-theme' ) );
$this->checkbox_field( $instance, 'enable_cache', esc_html__( 'Enable Cache', 'rx-theme' ) );
?>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'cache_time' ) ); ?>">
<?php esc_html_e( 'Cache Time in Minutes:', 'rx-theme' ); ?>
</label>
<input class="tiny-text"
id="<?php echo esc_attr( $this->get_field_id( 'cache_time' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'cache_time' ) ); ?>"
type="number"
min="1"
max="1440"
value="<?php echo esc_attr( absint( $instance['cache_time'] ) ); ?>">
</p>
<hr>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'layout_style' ) ); ?>">
<?php esc_html_e( 'Layout Style:', 'rx-theme' ); ?>
</label>
<select class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'layout_style' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'layout_style' ) ); ?>">
<?php foreach ( $layout_options as $key => $label ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $instance['layout_style'], $key ); ?>>
<?php echo esc_html( $label ); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'wrapper_class' ) ); ?>">
<?php esc_html_e( 'Extra Wrapper Class:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'wrapper_class' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'wrapper_class' ) ); ?>"
type="text"
value="<?php echo esc_attr( $instance['wrapper_class'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'item_class' ) ); ?>">
<?php esc_html_e( 'Extra Item Class:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'item_class' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'item_class' ) ); ?>"
type="text"
value="<?php echo esc_attr( $instance['item_class'] ); ?>">
</p>
<hr>
<p>
<input class="checkbox"
id="<?php echo esc_attr( $this->get_field_id( 'show_view_all' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'show_view_all' ) ); ?>"
type="checkbox"
value="1" <?php checked( $instance['show_view_all'], 1 ); ?>>
<label for="<?php echo esc_attr( $this->get_field_id( 'show_view_all' ) ); ?>">
<?php esc_html_e( 'Show View All Link', 'rx-theme' ); ?>
</label>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'view_all_text' ) ); ?>">
<?php esc_html_e( 'View All Text:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'view_all_text' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'view_all_text' ) ); ?>"
type="text"
value="<?php echo esc_attr( $instance['view_all_text'] ); ?>">
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'view_all_url' ) ); ?>">
<?php esc_html_e( 'Custom View All URL:', 'rx-theme' ); ?>
</label>
<input class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'view_all_url' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'view_all_url' ) ); ?>"
type="url"
value="<?php echo esc_url( $instance['view_all_url'] ); ?>">
</p>
<hr>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'before_item' ) ); ?>">
<?php esc_html_e( 'Before Each Item HTML:', 'rx-theme' ); ?>
</label>
<textarea class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'before_item' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'before_item' ) ); ?>"
rows="2"><?php echo esc_textarea( $instance['before_item'] ); ?></textarea>
</p>
<p>
<label for="<?php echo esc_attr( $this->get_field_id( 'after_item' ) ); ?>">
<?php esc_html_e( 'After Each Item HTML:', 'rx-theme' ); ?>
</label>
<textarea class="widefat"
id="<?php echo esc_attr( $this->get_field_id( 'after_item' ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( 'after_item' ) ); ?>"
rows="2"><?php echo esc_textarea( $instance['after_item'] ); ?></textarea>
</p>
</div>
<?php
}
/**
* Save widget settings.
*/
public function update( $new_instance, $old_instance ) {
$instance = wp_parse_args( (array) $old_instance, $this->defaults );
$text_fields = array(
'title',
'post_type',
'orderby',
'order',
'category_ids',
'tag_ids',
'taxonomy',
'taxonomy_terms',
'author_ids',
'include_posts',
'exclude_posts',
'date_after',
'date_before',
'thumbnail_size',
'thumbnail_position',
'layout_style',
'wrapper_class',
'item_class',
'view_all_text',
);
foreach ( $text_fields as $field ) {
$instance[ $field ] = isset( $new_instance[ $field ] ) ? sanitize_text_field( $new_instance[ $field ] ) : '';
}
$url_fields = array(
'fallback_image',
'view_all_url',
);
foreach ( $url_fields as $field ) {
$instance[ $field ] = isset( $new_instance[ $field ] ) ? esc_url_raw( $new_instance[ $field ] ) : '';
}
$html_fields = array(
'before_item',
'after_item',
);
foreach ( $html_fields as $field ) {
$instance[ $field ] = isset( $new_instance[ $field ] ) ? wp_kses_post( $new_instance[ $field ] ) : '';
}
$number_fields = array(
'posts_per_page',
'offset',
'thumbnail_width',
'thumbnail_height',
'title_words',
'excerpt_words',
'cache_time',
);
foreach ( $number_fields as $field ) {
$instance[ $field ] = isset( $new_instance[ $field ] ) ? absint( $new_instance[ $field ] ) : absint( $this->defaults[ $field ] );
}
$checkbox_fields = array(
'show_thumbnail',
'show_title',
'show_excerpt',
'show_date',
'show_modified_date',
'show_author',
'show_comments',
'show_category',
'show_tags',
'show_reading_time',
'show_view_all',
'open_new_tab',
'nofollow_links',
'ignore_sticky',
'only_sticky',
'hide_current_post',
'enable_cache',
);
foreach ( $checkbox_fields as $field ) {
$instance[ $field ] = ! empty( $new_instance[ $field ] ) ? 1 : 0;
}
$instance['posts_per_page'] = min( 50, max( 1, absint( $instance['posts_per_page'] ) ) );
$instance['title_words'] = min( 50, max( 1, absint( $instance['title_words'] ) ) );
$instance['excerpt_words'] = min( 100, max( 1, absint( $instance['excerpt_words'] ) ) );
$instance['cache_time'] = min( 1440, max( 1, absint( $instance['cache_time'] ) ) );
$instance['thumbnail_width'] = max( 20, absint( $instance['thumbnail_width'] ) );
$instance['thumbnail_height'] = max( 20, absint( $instance['thumbnail_height'] ) );
$this->flush_widget_cache();
return $instance;
}
/**
* Build WP_Query args.
*/
protected function build_query_args( $instance ) {
$post_type = ! empty( $instance['post_type'] ) ? sanitize_key( $instance['post_type'] ) : 'post';
$query_args = array(
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => absint( $instance['posts_per_page'] ),
'offset' => absint( $instance['offset'] ),
'orderby' => sanitize_key( $instance['orderby'] ),
'order' => 'ASC' === strtoupper( $instance['order'] ) ? 'ASC' : 'DESC',
'ignore_sticky_posts' => ! empty( $instance['ignore_sticky'] ),
'no_found_rows' => true,
'update_post_meta_cache' => true,
'update_post_term_cache' => true,
);
if ( 'rand' === $query_args['orderby'] ) {
$query_args['orderby'] = 'rand';
}
if ( ! empty( $instance['include_posts'] ) ) {
$query_args['post__in'] = $this->csv_to_absint_array( $instance['include_posts'] );
$query_args['orderby'] = 'post__in';
}
if ( ! empty( $instance['exclude_posts'] ) ) {
$query_args['post__not_in'] = $this->csv_to_absint_array( $instance['exclude_posts'] );
}
if ( ! empty( $instance['hide_current_post'] ) && is_singular() ) {
$current_id = get_queried_object_id();
if ( $current_id ) {
if ( empty( $query_args['post__not_in'] ) ) {
$query_args['post__not_in'] = array();
}
$query_args['post__not_in'][] = absint( $current_id );
}
}
if ( ! empty( $instance['only_sticky'] ) ) {
$sticky_posts = get_option( 'sticky_posts' );
if ( ! empty( $sticky_posts ) && is_array( $sticky_posts ) ) {
$query_args['post__in'] = array_map( 'absint', $sticky_posts );
} else {
$query_args['post__in'] = array( 0 );
}
}
if ( ! empty( $instance['category_ids'] ) ) {
$query_args['category__in'] = $this->csv_to_absint_array( $instance['category_ids'] );
}
if ( ! empty( $instance['tag_ids'] ) ) {
$query_args['tag__in'] = $this->csv_to_absint_array( $instance['tag_ids'] );
}
if ( ! empty( $instance['author_ids'] ) ) {
$query_args['author__in'] = $this->csv_to_absint_array( $instance['author_ids'] );
}
$tax_query = array();
if ( ! empty( $instance['taxonomy'] ) && ! empty( $instance['taxonomy_terms'] ) && taxonomy_exists( $instance['taxonomy'] ) ) {
$tax_query[] = array(
'taxonomy' => sanitize_key( $instance['taxonomy'] ),
'field' => 'term_id',
'terms' => $this->csv_to_absint_array( $instance['taxonomy_terms'] ),
);
}
if ( ! empty( $tax_query ) ) {
$query_args['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
}
$date_query = array();
if ( ! empty( $instance['date_after'] ) ) {
$date_query['after'] = sanitize_text_field( $instance['date_after'] );
}
if ( ! empty( $instance['date_before'] ) ) {
$date_query['before'] = sanitize_text_field( $instance['date_before'] );
}
if ( ! empty( $date_query ) ) {
$date_query['inclusive'] = true;
$query_args['date_query'][] = $date_query;
}
/**
* Filter query args for RX recent posts widget.
*/
return apply_filters( 'rx_recent_posts_widget_query_args', $query_args, $instance, $this );
}
/**
* Thumbnail HTML.
*/
protected function get_thumbnail_html( $post_id, $instance, $link_attrs ) {
if ( 'none' === $instance['thumbnail_position'] ) {
return '';
}
$width = absint( $instance['thumbnail_width'] );
$height = absint( $instance['thumbnail_height'] );
$size = ! empty( $instance['thumbnail_size'] ) ? sanitize_key( $instance['thumbnail_size'] ) : 'thumbnail';
$image_html = '';
if ( has_post_thumbnail( $post_id ) ) {
$image_html = get_the_post_thumbnail(
$post_id,
$size,
array(
'class' => 'rx-recent-post-thumbnail-img',
'alt' => esc_attr( get_the_title( $post_id ) ),
'loading' => 'lazy',
'decoding' => 'async',
'style' => 'width:' . $width . 'px;height:' . $height . 'px;object-fit:cover;',
'itemprop' => 'image',
)
);
} elseif ( ! empty( $instance['fallback_image'] ) ) {
$image_html = sprintf(
'<img class="rx-recent-post-thumbnail-img rx-fallback-img" src="%1$s" alt="%2$s" loading="lazy" decoding="async" style="width:%3$dpx;height:%4$dpx;object-fit:cover;" itemprop="image">',
esc_url( $instance['fallback_image'] ),
esc_attr( get_the_title( $post_id ) ),
$width,
$height
);
}
if ( empty( $image_html ) ) {
return '';
}
return sprintf(
'<a class="rx-recent-post-thumbnail" href="%1$s" %2$s aria-label="%3$s">%4$s</a>',
esc_url( get_permalink( $post_id ) ),
$link_attrs,
esc_attr( get_the_title( $post_id ) ),
$image_html
);
}
/**
* Meta HTML.
*/
protected function get_meta_html( $post_id, $instance ) {
$meta = array();
if ( ! empty( $instance['show_date'] ) ) {
$meta[] = sprintf(
'<span class="rx-post-meta-date"><time datetime="%1$s" itemprop="datePublished">%2$s</time></span>',
esc_attr( get_the_date( DATE_W3C, $post_id ) ),
esc_html( get_the_date( '', $post_id ) )
);
}
if ( ! empty( $instance['show_modified_date'] ) ) {
$meta[] = sprintf(
'<span class="rx-post-meta-modified">%1$s <time datetime="%2$s" itemprop="dateModified">%3$s</time></span>',
esc_html__( 'Updated:', 'rx-theme' ),
esc_attr( get_the_modified_date( DATE_W3C, $post_id ) ),
esc_html( get_the_modified_date( '', $post_id ) )
);
}
if ( ! empty( $instance['show_author'] ) ) {
$author_id = get_post_field( 'post_author', $post_id );
$meta[] = sprintf(
'<span class="rx-post-meta-author" itemprop="author" itemscope itemtype="https://schema.org/Person">%1$s <a href="%2$s" itemprop="url"><span itemprop="name">%3$s</span></a></span>',
esc_html__( 'By', 'rx-theme' ),
esc_url( get_author_posts_url( $author_id ) ),
esc_html( get_the_author_meta( 'display_name', $author_id ) )
);
}
if ( ! empty( $instance['show_comments'] ) && comments_open( $post_id ) ) {
$comments_number = get_comments_number( $post_id );
$meta[] = sprintf(
'<span class="rx-post-meta-comments"><a href="%1$s">%2$s</a></span>',
esc_url( get_comments_link( $post_id ) ),
esc_html( sprintf( _n( '%s Comment', '%s Comments', $comments_number, 'rx-theme' ), number_format_i18n( $comments_number ) ) )
);
}
if ( ! empty( $instance['show_category'] ) ) {
$categories = get_the_category_list( ', ', '', $post_id );
if ( ! empty( $categories ) ) {
$meta[] = '<span class="rx-post-meta-categories">' . wp_kses_post( $categories ) . '</span>';
}
}
if ( ! empty( $instance['show_tags'] ) ) {
$tags = get_the_tag_list( '', ', ', '', $post_id );
if ( ! empty( $tags ) ) {
$meta[] = '<span class="rx-post-meta-tags">' . wp_kses_post( $tags ) . '</span>';
}
}
if ( ! empty( $instance['show_reading_time'] ) ) {
$meta[] = sprintf(
'<span class="rx-post-meta-reading-time">%s</span>',
esc_html( $this->get_reading_time( $post_id ) )
);
}
if ( empty( $meta ) ) {
return '';
}
return '<div class="rx-recent-post-meta">' . implode( '<span class="rx-meta-separator"> / </span>', $meta ) . '</div>';
}
/**
* Get excerpt.
*/
protected function get_excerpt( $post_id, $words = 18 ) {
$excerpt = get_the_excerpt( $post_id );
if ( empty( $excerpt ) ) {
$excerpt = wp_strip_all_tags( get_post_field( 'post_content', $post_id ) );
}
return $this->trim_words( $excerpt, $words );
}
/**
* Reading time.
*/
protected function get_reading_time( $post_id ) {
$content = wp_strip_all_tags( get_post_field( 'post_content', $post_id ) );
$words = str_word_count( $content );
$minutes = max( 1, ceil( $words / 200 ) );
return sprintf(
_n( '%s min read', '%s mins read', $minutes, 'rx-theme' ),
number_format_i18n( $minutes )
);
}
/**
* Trim words.
*/
protected function trim_words( $text, $words = 12 ) {
$words = max( 1, absint( $words ) );
return wp_trim_words( wp_strip_all_tags( $text ), $words, '…' );
}
/**
* Link attributes.
*/
protected function get_link_attrs( $instance ) {
$attrs = array();
if ( ! empty( $instance['open_new_tab'] ) ) {
$attrs[] = 'target="_blank"';
$attrs[] = 'rel="noopener noreferrer' . ( ! empty( $instance['nofollow_links'] ) ? ' nofollow' : '' ) . '"';
} elseif ( ! empty( $instance['nofollow_links'] ) ) {
$attrs[] = 'rel="nofollow"';
}
return implode( ' ', $attrs );
}
/**
* CSV to absint array.
*/
protected function csv_to_absint_array( $csv ) {
if ( empty( $csv ) ) {
return array();
}
$items = explode( ',', $csv );
$items = array_map( 'trim', $items );
$items = array_map( 'absint', $items );
$items = array_filter( $items );
return array_values( array_unique( $items ) );
}
/**
* Checkbox field helper.
*/
protected function checkbox_field( $instance, $field, $label ) {
?>
<p>
<input class="checkbox"
id="<?php echo esc_attr( $this->get_field_id( $field ) ); ?>"
name="<?php echo esc_attr( $this->get_field_name( $field ) ); ?>"
type="checkbox"
value="1" <?php checked( ! empty( $instance[ $field ] ), 1 ); ?>>
<label for="<?php echo esc_attr( $this->get_field_id( $field ) ); ?>">
<?php echo esc_html( $label ); ?>
</label>
</p>
<?php
}
/**
* Cache key.
*/
protected function get_cache_key( $args, $instance ) {
$key_data = array(
'id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->id,
'instance' => $instance,
'page' => is_singular() ? get_queried_object_id() : 0,
'lang' => function_exists( 'pll_current_language' ) ? pll_current_language() : get_locale(),
);
return 'rx_recent_posts_widget_' . md5( wp_json_encode( $key_data ) );
}
/**
* Flush widget cache.
*
* Note: Transients with dynamic md5 keys cannot be deleted one by one easily
* without direct DB search. This method uses version bumping approach through option.
*/
public function flush_widget_cache() {
update_option( 'rx_recent_posts_widget_cache_version', time(), false );
}
}
endif;
/**
* Register widget.
*/
if ( ! function_exists( 'rx_register_advanced_recent_posts_widget' ) ) {
function rx_register_advanced_recent_posts_widget() {
register_widget( 'RX_Advanced_Recent_Posts_Widget' );
}
}
add_action( 'widgets_init', 'rx_register_advanced_recent_posts_widget' );
One important improvement: your current cache key needs to use the cache version so cache is truly flushed after post updates. Replace the get_cache_key() method above with this improved version:
protected function get_cache_key( $args, $instance ) {
$cache_version = get_option( 'rx_recent_posts_widget_cache_version', '1' );
$key_data = array(
'id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->id,
'instance' => $instance,
'page' => is_singular() ? get_queried_object_id() : 0,
'lang' => function_exists( 'pll_current_language' ) ? pll_current_language() : get_locale(),
'cache_version' => $cache_version,
);
return 'rx_recent_posts_widget_' . md5( wp_json_encode( $key_data ) );
}
Also add this small CSS in your theme, for example:
assets/css/widgets/recent-posts-widget.css
.rx-recent-posts-list {
list-style: none;
margin: 0;
padding: 0;
}
.rx-recent-post-item {
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.rx-recent-post-article {
display: flex;
gap: 12px;
align-items: flex-start;
}
.rx-thumb-right .rx-recent-post-article {
flex-direction: row-reverse;
}
.rx-thumb-top .rx-recent-post-article {
display: block;
}
.rx-recent-post-thumbnail {
display: block;
flex: 0 0 auto;
overflow: hidden;
border-radius: 8px;
}
.rx-recent-post-thumbnail-img {
display: block;
max-width: 100%;
height: auto;
}
.rx-recent-post-title {
margin: 0 0 6px;
font-size: 16px;
line-height: 1.35;
}
.rx-recent-post-title a {
text-decoration: none;
}
.rx-recent-post-meta {
font-size: 13px;
line-height: 1.5;
opacity: 0.75;
margin-bottom: 6px;
}
.rx-recent-post-excerpt {
font-size: 14px;
line-height: 1.6;
}
.rx-recent-posts-layout-grid .rx-recent-posts-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.rx-recent-posts-layout-grid .rx-recent-post-item {
margin: 0;
padding: 0;
border: 0;
}
.rx-recent-posts-layout-card .rx-recent-post-item {
padding: 14px;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 12px;
}
.rx-recent-posts-layout-compact .rx-recent-post-item {
margin-bottom: 10px;
padding-bottom: 10px;
}
.rx-recent-posts-layout-minimal .rx-recent-post-thumbnail,
.rx-recent-posts-layout-minimal .rx-recent-post-meta,
.rx-recent-posts-layout-minimal .rx-recent-post-excerpt {
display: none;
}
.rx-recent-posts-view-all {
margin-top: 15px;
}
.rx-recent-posts-view-all-link {
display: inline-flex;
align-items: center;
text-decoration: none;
font-weight: 600;
}
@media (max-width: 600px) {
.rx-recent-posts-layout-grid .rx-recent-posts-list {
grid-template-columns: 1fr;
}
.rx-recent-post-article {
gap: 10px;
}
}
Then enqueue the CSS from your theme functions.php:
function rx_enqueue_recent_posts_widget_css() {
wp_enqueue_style(
'rx-recent-posts-widget',
get_template_directory_uri() . '/assets/css/widgets/recent-posts-widget.css',
array(),
wp_get_theme()->get( 'Version' )
);
}
add_action( 'wp_enqueue_scripts', 'rx_enqueue_recent_posts_widget_css' );
And make sure your theme loads the widget file in functions.php:
require_once get_template_directory() . '/inc/widgets/recent-posts-widget.php';