Top Tags

Custom base RSS feed for WordPress

Custom base RSS feed for WordPress with advanced RSS 2.0 implementation, timezone handling, and feed registration

Overview

This guide provides a comprehensive approach to creating custom RSS feeds in WordPress with proper XML encoding, timezone management, and advanced content delivery. RSS (Really Simple Syndication) feeds are XML-based formats that allow content distribution and syndication across various platforms and feed readers.

Key Concepts

RSS 2.0 Specification

RSS 2.0 is a widely supported format that defines a standardized structure for distributing content. Each feed contains:

  • Channel: The feed metadata container
  • Items: Individual content entries (posts)
  • Required elements: title, link, description
  • Optional elements: pubDate, category, guid, enclosure

Content-Type Header

The Content-Type: application/rss+xml; charset=UTF-8 header informs clients that the response is an XML feed, ensuring proper parsing by feed readers and browsers.

CDATA Sections

CDATA (Character Data) sections allow inclusion of special XML characters without escaping. Example: <![CDATA[...]]> prevents interpretation of <, >, and & characters within content.

Implementation

Timezone Management

Set UTC +3 time zone

Understanding the Core RSS Generation

The following template generates a valid RSS 2.0 feed with the following capabilities:

  1. Dynamic Content Loading: Uses WP_Query to fetch posts with customizable parameters
  2. Metadata Extraction: Retrieves post data (title, permalink, excerpt, content, categories)
  3. Image Enclosures: Includes featured images as media enclosures for RSS readers supporting multimedia
  4. RFC 2822 Date Format: Publishes dates in the format required by RSS 2.0 specification (RFC 2822)
  5. Custom Timezone Offset: Appends timezone offset (+0300) to accommodate non-UTC zones

WP_Query Parameters

The $args array in the code uses:

  • post_type: Specifies post type(s) to fetch (default: 'post', can include custom post types)
  • posts_per_page: Limits feed items to prevent excessive data transfer
  • Additional parameters can include: orderby, order, meta_query, tax_query for filtering

Date Formatting

The pubDate element uses PHP's strtotime() to parse WordPress post dates and date() to format as RFC 2822-compliant timestamp. The formula get_the_time('Y-m-d H:i:s') . ' +0 hours' adjusts timezone offset.

php
1<?php
2header('Content-Type: application/rss+xml; charset=UTF-8');
3
4echo '<?xml version="1.0" encoding="UTF-8" ?>';
5
6?>
7<rss version="2.0">
8 <channel>
9 <title><?php echo get_bloginfo('name'); ?></title>
10 <link><?php echo get_bloginfo('url'); ?></link>
11 <description>Your custom feed description</description>
12 <language>en-us</language>
13
14 <?php
15 $args = array(
16 'post_type' => 'post', // Fetch posts or any custom post types
17 'posts_per_page' => 10 // Limit the number of posts
18 );
19 $custom_query = new WP_Query($args);
20
21 if ($custom_query->have_posts()):
22 while ($custom_query->have_posts()):
23 $custom_query->the_post();
24 ?>
25 <item>
26 <title><?php the_title_rss(); ?></title>
27 <link><?php the_permalink_rss(); ?></link>
28 <description><![CDATA[<?php the_excerpt_rss(); ?>]]></description>
29 <pubDate>
30 <?php echo date('D, d M Y H:i:s', strtotime(get_the_time('Y-m-d H:i:s') . ' +0 hours')) . ' +0300'; ?>
31 </pubDate>
32 <?php
33 // Get post categories
34 $categories = get_the_category();
35 if (!empty($categories)):
36 foreach ($categories as $category):
37 ?>
38 <category><?php echo $category->name; ?></category>
39 <?php
40 endforeach;
41 endif;
42 ?>
43 <full-text>
44 <![CDATA[
45 <?php echo apply_filters('the_content', get_the_content()); ?>
46 ]]>
47 </full-text>
48
49 <?php if (has_post_thumbnail()):
50 $thumbnail_url = get_the_post_thumbnail_url(get_the_ID(), 'full'); // Get the featured image URL
51 ?>
52 <enclosure url="<?php echo esc_url($thumbnail_url); ?>" type="image/jpeg" />
53 <?php endif; ?>
54 <guid isPermaLink="false"><?php the_guid(); ?></guid>
55 </item>
56 <?php
57 endwhile;
58 endif;
59 wp_reset_postdata();
60 ?>
61 </channel>
62</rss>

Create a new feed custom.php file in the theme folder. functions.php:

php
1function custom_feed_init() {
2 add_feed('custom', 'generate_custom_feed');
3}
4add_action('init', 'custom_feed_init');
5
6function generate_custom_feed() {
7 get_template_part('custom');
8}

Feed Registration Architecture

The add_feed() function registers a new feed type in WordPress's rewrite rules. When a user accesses yoursite.com/feed/custom/, WordPress triggers the callback function specified as the second parameter.

Hook Execution Order:

  1. init hook (priority 10): Feed types are registered during WordPress initialization
  2. WordPress processes rewrite rules and matches feed requests
  3. Callback function executes to output feed content

Why get_template_part('custom')?

  • Locates the custom.php template file in the active theme directory
  • Applies template hierarchy and theme filters
  • Allows feed file access through WordPress's data structures and nonces

Feed URL Structure

By default, custom feeds are accessible via:

yoursite.com/feed/custom/

WordPress automatically appends this route after registering via add_feed(). The custom parameter becomes part of the feed query variable accessible in $wp->query_vars['feed'].

WordPress Feed Hooks and Lifecycle

Understanding WordPress's feed generation lifecycle helps optimize and customize RSS feeds. The feed processing follows this sequence:

Feed Hook Execution Order

  1. Initialization Phase (init hook, priority 10)

    • add_feed() registers feed type with rewrite rules
    • WordPress core feeds (feed, rss, rss2, atom, rdf) are available by default
  2. Request Parsing Phase (parse_request hook)

    • WordPress matches URL against registered feed patterns
    • Feed query variable (wp->query_vars['feed']) is populated
    • Example: yoursite.com/feed/custom/ sets query_vars['feed'] = 'custom'
  3. Template Loading Phase

    • WordPress locates template file via get_template_part()
    • Child theme custom.php checked first, then parent theme
    • Falls back to theme root if custom.php not found
  4. Content Output Phase

    • Template file executes PHP code
    • Content filters and actions fire (e.g., the_content, the_excerpt)
    • RSS XML output sent with proper Content-Type header

WordPress Core Feed Hooks

Built-in hooks available for all feed types:

php
1// Fires before feed content is generated
2do_action( 'feed_head' );
3
4// Fires for each item in RSS/Atom feeds
5do_action( 'rss2_item' );
6do_action( 'atom_entry' );
7
8// Fires at end of feed generation
9do_action( 'feed_tail' );

Feed-Specific Query Hooks

Customize feed queries using these filters:

php
1// Modify WHERE clause for feed queries
2apply_filters( 'posts_where_paged', $where, $query );
3
4// Modify JOIN clause
5apply_filters( 'posts_join_paged', $join, $query );
6
7// Modify ORDER BY clause
8apply_filters( 'posts_orderby', $orderby, $query );
9
10// Modify GROUP BY clause
11apply_filters( 'posts_groupby', $groupby, $query );
12
13// Skip filters entirely for direct database queries
14'suppress_filters' => true

Method 1: Query Variable Interception

The following approach intercepts WordPress query variables at parse time:

php
1add_action('parse_request', function($wp) {
2 if (isset($wp->query_vars['feed']) && isset($_GET['custom'])) {
3 error_log('Custom feed hook triggered');
4 load_template(get_template_directory() . '/custom.php');
5 exit;
6 }
7});

Technical Details:

  • parse_request hook: Fires after WordPress finishes parsing the request object before the main query loop
  • Query Variable Inspection: $wp->query_vars['feed'] contains the feed slug (e.g., 'custom')
  • GET Parameter Check: $_GET['custom'] allows query string-based feed triggers (e.g., /?feed=custom&custom=1)
  • Template Loading: load_template() directly includes the feed file while maintaining WordPress context
  • Exit Statement: Prevents WordPress from continuing normal page rendering

Using add_rewrite_rule() provides cleaner URL structures:

php
1function custom_feed_rewrite_rules() {
2 add_rewrite_rule(
3 '^feed/custom/?$',
4 'index.php?feed=custom',
5 'top'
6 );
7
8 add_rewrite_rule(
9 '^feed/custom/page/([0-9]+)/?$',
10 'index.php?feed=custom&paged=$matches[1]',
11 'top'
12 );
13}
14
15add_action('init', 'custom_feed_rewrite_rules');
16flush_rewrite_rules(); // Call once, then remove to prevent performance impact

URL Patterns Generated:

  • /feed/custom/ → Routes to custom feed
  • /feed/custom/page/2/ → Supports pagination in feeds

Method 3: Conditional Feed Output

For more control, conditionally output different feed types:

php
1function generate_custom_feed() {
2 if (isset($_GET['type']) && $_GET['type'] === 'full') {
3 get_template_part('custom-full'); // Full content feed
4 } elseif (isset($_GET['type']) && $_GET['type'] === 'excerpt') {
5 get_template_part('custom-excerpt'); // Excerpt-only feed
6 } else {
7 get_template_part('custom'); // Default feed
8 }
9}
10
11// Access via: yoursite.com/feed/custom/?type=full

Performance Optimization Techniques

Caching RSS Output

RSS feeds are frequently accessed by aggregators. Implementing caching reduces database queries:

php
1function generate_custom_feed() {
2 $cache_key = 'custom_feed_output';
3 $cached_feed = get_transient($cache_key);
4
5 if ($cached_feed) {
6 echo $cached_feed;
7 exit;
8 }
9
10 ob_start();
11 get_template_part('custom');
12 $feed_output = ob_get_clean();
13
14 set_transient($cache_key, $feed_output, HOUR_IN_SECONDS);
15 echo $feed_output;
16}
17
18// Clear cache on post updates
19add_action('save_post', function() {
20 delete_transient('custom_feed_output');
21});

Key Benefits:

  • Reduces SQL queries for feed aggregators
  • Improves response time for first-time readers
  • Caches expire automatically (1 hour default)
  • Transient automatically deletes after expiration

Limiting Feed Items

Fetch only necessary posts to reduce bandwidth:

php
1$args = array(
2 'post_type' => 'post',
3 'posts_per_page' => 20, // Limit to 20 items
4 'orderby' => 'modified', // Sort by last modified
5 'order' => 'DESC', // Most recent first
6 'post_status' => 'publish', // Only published posts
7 'no_found_rows' => true, // Skip total row count (faster)
8 'update_post_meta_cache' => false, // Skip unnecessary metadata loading
9 'update_post_term_cache' => false, // Skip term cache
10);

XML Validation and Best Practices

XML Declaration and Encoding

Always specify encoding explicitly:

xml
1<?xml version="1.0" encoding="UTF-8"?>

Why it matters:

  • Declares XML format version (always 1.0 for RSS compatibility)
  • Encoding must match HTTP header Content-Type: application/rss+xml; charset=UTF-8
  • Mismatch causes parsing errors in strict feed readers

Escaping Special Characters

HTML content in RSS must be properly escaped:

php
1// ❌ Incorrect - XML parsing fails
2<description><?php echo $content; ?></description>
3
4// ✅ Correct - Escapes all special characters
5<description><?php echo esc_html($content); ?></description>
6
7// ✅ Best - Uses CDATA for formatted content
8<description><![CDATA[<?php echo apply_filters('the_content', $content); ?>]]></description>

Feed Validation

Test feeds using W3C Feed Validator:

https://validator.w3.org/feed/

Common issues detected:

  • Missing required elements (title, link, description)
  • Invalid date formats (must be RFC 2822)
  • Improper character encoding
  • Duplicate GUIDs

WP_Query Advanced Filtering

Filter by Category

php
1$args = array(
2 'post_type' => 'post',
3 'posts_per_page' => 10,
4 'tax_query' => array(
5 array(
6 'taxonomy' => 'category',
7 'field' => 'slug',
8 'terms' => 'news', // Only "news" category
9 ),
10 ),
11);

WP_Query Fundamentals and Architecture

The WP_Query class is the core mechanism for retrieving posts in WordPress. Understanding its architecture enables efficient feed queries:

Constructor and Initialization:

php
1public function __construct( $query = '' ) {
2 if ( ! empty( $query ) ) {
3 $this->query( $query );
4 }
5}

The constructor accepts either:

  • String format: 'post_type=post&posts_per_page=10'
  • Array format: array( 'post_type' => 'post', 'posts_per_page' => 10 )

Key Properties After Query:

  • $query->posts - Array of post objects retrieved
  • $query->post_count - Number of posts in current result set
  • $query->found_posts - Total posts matching query (ignoring pagination)
  • $query->max_num_pages - Total number of pages available
  • $query->query_vars - Array of parsed query parameters

Query Processing Pipeline:

php
1$query = new WP_Query( $args );
2// Internal flow:
3// 1. __construct() called
4// 2. query() method invoked
5// 3. parse_query() processes arguments
6// 4. Query filters applied (if suppress_filters is false)
7// 5. get_posts() executes SQL query
8// 6. Results stored in $query->posts

Essential Query Parameters for Feed Generation

Fundamental Parameters:

php
1$args = array(
2 // ========== CONTENT SELECTION ==========
3 'post_type' => array( 'post', 'page' ), // Multiple types supported
4 'post_status' => 'publish', // Only published posts
5 'posts_per_page' => 20, // Feed item limit
6
7 // ========== ORDERING AND PAGINATION ==========
8 'orderby' => 'modified', // Sort by last modified date
9 'order' => 'DESC', // Most recent first
10 'paged' => 1, // Page number for pagination
11
12 // ========== PERFORMANCE OPTIMIZATION ==========
13 'no_found_rows' => true, // Skip counting total rows (faster)
14 'suppress_filters' => false, // Apply standard filters
15 'update_post_meta_cache' => false, // Skip meta cache (unless needed)
16 'update_post_term_cache' => false, // Skip term cache (unless needed)
17);

Performance Impact of Parameters:

  • no_found_rows: true - Removes SQL_CALC_FOUND_ROWS (significant for large datasets)
  • suppress_filters: true - Bypasses all WordPress filters (fastest, but less flexible)
  • update_post_meta_cache: false - Avoids loading custom fields if not displayed
  • update_post_term_cache: false - Avoids taxonomy loading if not displayed

Filter by Post Meta

php
1$args = array(
2 'post_type' => 'post',
3 'posts_per_page' => 10,
4 'meta_query' => array(
5 array(
6 'key' => '_featured', // Custom meta key
7 'value' => '1',
8 'compare' => '=',
9 ),
10 ),
11);

Meta Query Comparison Operators:

php
1$args = array(
2 'post_type' => 'product',
3 'meta_query' => array(
4 // Exact match
5 array(
6 'key' => 'price',
7 'value' => '100',
8 'compare' => '=',
9 'type' => 'numeric',
10 ),
11
12 // Range queries
13 array(
14 'key' => 'price',
15 'value' => array( 20, 100 ),
16 'compare' => 'BETWEEN',
17 'type' => 'numeric',
18 ),
19
20 // Pattern matching
21 array(
22 'key' => 'color',
23 'value' => 'blue',
24 'compare' => 'LIKE',
25 ),
26
27 // NOT LIKE pattern
28 array(
29 'key' => 'color',
30 'value' => 'blue',
31 'compare' => 'NOT LIKE',
32 ),
33
34 // Field existence check
35 array(
36 'key' => '_featured',
37 'compare' => 'EXISTS',
38 ),
39 ),
40 'meta_compare' => 'AND', // Combine conditions (AND/OR)
41);

Meta Query Relation Logic:

  • meta_compare: 'AND' - All conditions must be true
  • meta_compare: 'OR' - At least one condition must be true

Exclude Posts

php
1$args = array(
2 'post_type' => 'post',
3 'posts_per_page' => 10,
4 'post__not_in' => array(1, 2, 3), // Exclude specific post IDs
5);

Advanced Exclusion Patterns:

php
1$args = array(
2 'post_type' => 'post',
3
4 // Exclude specific posts
5 'post__not_in' => array(1, 2, 3),
6
7 // Include only specific posts
8 'post__in' => array(5, 10, 15),
9
10 // Exclude sticky posts
11 'post__not_in' => get_option( 'sticky_posts' ),
12
13 // Exclude author
14 'author__not_in' => array(1, 2),
15
16 // Exclude taxonomy terms
17 'tax_query' => array(
18 array(
19 'taxonomy' => 'category',
20 'field' => 'id',
21 'terms' => array(1, 2, 3),
22 'operator' => 'NOT IN', // Exclude these categories
23 ),
24 ),
25);

Advanced Taxonomy Filtering

php
1$args = array(
2 'post_type' => 'post',
3 'tax_query' => array(
4 'relation' => 'AND', // Combine multiple taxonomy conditions
5 array(
6 'taxonomy' => 'category',
7 'field' => 'slug',
8 'terms' => array( 'technology', 'science' ),
9 'operator' => 'IN', // Match any term
10 ),
11 array(
12 'taxonomy' => 'post_tag',
13 'field' => 'id',
14 'terms' => array( 5, 10 ),
15 'operator' => 'NOT IN', // Exclude these tags
16 ),
17 ),
18);

Taxonomy Query Operators:

  • IN - Match any of the terms
  • NOT IN - Exclude these terms
  • AND - Match all terms

Search and Text Filtering

php
1$args = array(
2 'post_type' => 'post',
3 's' => 'keyword', // Full-text search
4 'sentence' => true, // Search exact phrase
5);

Search Behavior:

  • 's' => 'keyword' - Searches post_title and post_content
  • 'sentence' => true - Treats search value as exact phrase
  • Search is case-insensitive
  • Searchable custom post types can override behavior

Date-Based Queries

php
1$args = array(
2 'post_type' => 'post',
3 'year' => 2024, // Posts from 2024
4 'monthnum' => 12, // December
5 'day' => 25, // 25th day
6 'w' => 1, // Week number
7 'before' => array(
8 'year' => 2024,
9 'month' => 12,
10 'day' => 31,
11 ),
12 'after' => array(
13 'year' => 2024,
14 'month' => 1,
15 'day' => 1,
16 ),
17);

Date Query Comparison:

php
1$args = array(
2 'post_type' => 'post',
3 'date_query' => array(
4 array(
5 'after' => '2024-01-01',
6 'before' => '2024-12-31',
7 'inclusive' => true,
8 ),
9 ),
10);

Keyword Search with WP_Query

The s parameter enables full-text search across post content:

php
1$query = new WP_Query( array( 's' => 'keyword' ) );

This searches in:

  • post_title - Post title field
  • post_content - Main content area
  • Can be extended via filters

Display Post by Slug using WP_Query

Fetch content using URL-friendly identifiers:

php
1$query = new WP_Query( array( 'name' => 'about-my-life' ) );
php
1$query = new WP_Query( array( 'pagename' => 'contact' ) );

Difference:

  • 'name' - Works for posts and custom post types (uses post_name)
  • 'pagename' - Hierarchy-aware for pages (includes parent/child structure)

Testing and Debugging

Enable WordPress Debug Logging

In wp-config.php:

php
1define('WP_DEBUG', true);
2define('WP_DEBUG_LOG', true);
3define('WP_DEBUG_DISPLAY', false); // Don't display errors on frontend

Feed debug messages appear in /wp-content/debug.log.

Verify Feed Generation

Check raw XML output:

bash
1curl -I https://yoursite.com/feed/custom/

Should return:

Content-Type: application/rss+xml; charset=UTF-8

Test with Multiple Feed Readers

  • Feedly: Popular online aggregator
  • Thunderbird: Desktop feed reader
  • Apple Podcasts: For podcast-style feeds
  • W3C Validator: XML structure validation

Comment RSS Feeds

WordPress generates separate feeds for post comments. Understanding comment feed hooks enables custom comment syndication:

Comment Feed Structure

php
1// Access comment feed for specific post
2/feed/comment-on/post-id/
3
4// Access comment feed for all posts
5/feed/comments/
6
7// Access comment feed in Atom format
8/feed/comments/atom/

Comment Feed Hooks

Built-in hooks fired during comment feed generation:

php
1// Fires at the end of each comment RSS2 item
2do_action( 'commentrss2_item', $comment_id, $comment_post_id );
3
4// Fires inside feed tag in Atom comment feed
5do_action( 'atom_comments_ns' );
6
7// Fires at end of Atom comment entry
8do_action( 'atom_entry' );

Comment Feed Query Filters

Customize comment feed queries at database level:

php
1// Filter WHERE clause for comment feeds
2apply_filters( 'comment_feed_where', $where );
3
4// Filter JOIN clause
5apply_filters( 'comment_feed_join', $join );
6
7// Filter ORDER BY clause
8apply_filters( 'comment_feed_orderby', $orderby );
9
10// Filter GROUP BY clause
11apply_filters( 'comment_feed_groupby', $groupby );
12
13// Filter LIMIT clause
14apply_filters( 'comment_feed_limits', $limits );

Custom Comment Feed Example

php
1function customize_comment_feed() {
2 add_filter( 'comment_feed_where', function( $where ) {
3 global $wpdb;
4
5 // Exclude comment spam
6 $where .= " AND {$wpdb->comments}.comment_approved = 1";
7
8 return $where;
9 });
10
11 add_filter( 'comment_feed_orderby', function( $orderby ) {
12 global $wpdb;
13
14 // Sort by most recent first
15 return "{$wpdb->comments}.comment_date DESC";
16 });
17}
18
19add_action( 'init', 'customize_comment_feed' );

Advanced Feed Hook Usage

Modify Default Feed Type

Control which feed format is served by default:

php
1add_filter( 'default_feed', function( $feed ) {
2 return 'rss2'; // Use RSS 2.0 by default (default is 'rss')
3});

Intercept and modify feed URLs generated by WordPress:

php
1// Modify post comments feed permalink
2add_filter( 'post_comments_feed_link', function( $feed_link, $post_id ) {
3 // Example: Use custom domain
4 return str_replace( 'example.com', 'feeds.example.com', $feed_link );
5}, 10, 2 );
6
7// Modify post type archive feed link
8add_filter( 'post_type_archive_feed_link', function( $feed_link, $post_type ) {
9 if ( 'product' === $post_type ) {
10 return home_url( '/shop-feed/' );
11 }
12 return $feed_link;
13}, 10, 2 );

Add Custom Namespaces to RSS Feeds

Extend RSS with custom XML namespaces for additional metadata:

php
1function add_custom_namespace_to_rss() {
2 add_action( 'rss2_ns', function() {
3 echo 'xmlns:content="http://purl.org/rss/1.0/modules/content/" ';
4 echo 'xmlns:media="http://search.yahoo.com/mrss/" ';
5 echo 'xmlns:custom="http://example.com/custom/" ';
6 });
7}
8
9add_action( 'init', 'add_custom_namespace_to_rss' );

Execute Custom Code on Feed Item Output

php
1add_action( 'rss2_item', function() {
2 global $post;
3
4 // Add custom metadata before item closes
5 echo '<custom:views>' . get_post_meta( $post->ID, 'views', true ) . '</custom:views>';
6 echo '<custom:rating>' . get_post_meta( $post->ID, 'rating', true ) . '</custom:rating>';
7});

Database Query Optimization for Feeds

Query Analysis and Profiling

Enable query debugging to identify inefficient queries:

php
1// Add to wp-config.php
2define( 'SAVEQUERIES', true );
3
4// In feed template:
5add_action( 'rss2_head', function() {
6 if ( current_user_can( 'manage_options' ) && defined( 'SAVEQUERIES' ) && SAVEQUERIES ) {
7 global $wpdb;
8 echo '<!-- Total Queries: ' . count( $wpdb->queries ) . ' -->' . "\n";
9
10 foreach ( $wpdb->queries as $query ) {
11 echo '<!-- ' . round( $query[1], 4 ) . 's: ' . $query[0] . ' -->' . "\n";
12 }
13 }
14});

Selective Field Loading

Retrieve only necessary columns from database:

php
1$args = array(
2 'post_type' => 'post',
3 'posts_per_page' => 20,
4 'fields' => 'ids', // Return only post IDs (fastest)
5);
6
7$query = new WP_Query( $args );
8// Now populate with required data
9foreach ( $query->posts as $post_id ) {
10 $post = get_post( $post_id );
11 // Load only needed meta fields
12 $excerpt = get_post_meta( $post_id, '_excerpt', true );
13}

Field Options:

  • 'posts' - Full post objects (default)
  • 'ids' - Post IDs only (fastest)
  • 'id=>parent' - Array with post ID as key, parent as value

Join Query Optimization

php
1add_filter( 'posts_join', function( $join, $query ) {
2 if ( $query->is_feed() ) {
3 global $wpdb;
4
5 // Only join postmeta if querying by meta
6 if ( isset( $query->query_vars['meta_key'] ) ) {
7 $join .= " LEFT JOIN {$wpdb->postmeta} ON {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id ";
8 }
9 }
10 return $join;
11}, 10, 2 );

Index Strategy for Feed Queries

WordPress uses these key indexes for feed optimization:

sql
1-- Primary key for fast post retrieval
2INDEX wp_posts_post_status_post_type_post_date (post_status, post_type, post_date)
3
4-- For meta queries
5INDEX wp_postmeta_post_id_meta_key (post_id, meta_key)
6
7-- For taxonomy queries
8INDEX wp_term_relationships_term_id (term_id)
9INDEX wp_term_relationships_term_taxonomy_id (term_taxonomy_id)
10
11-- For comment queries
12INDEX wp_comments_comment_approved_comment_post_id (comment_approved, comment_post_id)

RSS 2.0 Specification Details

Complete RSS 2.0 Element Reference

Required Channel Elements:

  • <title> - Feed title (max 1024 characters)
  • <link> - Website URL (valid URL required)
  • <description> - Feed description (max 1024 characters)

Optional Channel Elements:

  • <language> - Language code (ISO 639)
  • <copyright> - Copyright notice
  • <managingEditor> - Contact email
  • <webMaster> - Webmaster email
  • <pubDate> - Publication date (RFC 2822 format)
  • <lastBuildDate> - Last update date (RFC 2822 format)
  • <category> - Feed category
  • <docs> - Link to RSS documentation
  • <cloud> - Cloud protocol support
  • <ttl> - Time-to-live (minutes for cache)
  • <image> - Channel logo
  • <rating> - Content rating
  • <textInput> - Search form
  • <skipHours> - Hours to skip delivery
  • <skipDays> - Days to skip delivery

Item Elements:

  • <title> - Item title
  • <description> - Item content or excerpt (CDATA recommended)
  • <link> - Permanent link to item
  • <author> - Author email address
  • <category> - Item category
  • <comments> - URL to comments page
  • <enclosure> - Attached media (url, length, type)
  • <guid> - Global unique identifier
  • <pubDate> - Publication date (RFC 2822)
  • <source> - If republished from another feed

RFC 2822 Date Format Standard

All dates in RSS must follow RFC 2822 format:

Day, dd Mmm yyyy HH:MM:SS +0000 Mon, 06 Sep 2024 00:01:00 +0300

Format Breakdown:

  • Day - Three-letter day abbreviation (Mon-Sun)
  • dd - Two-digit day (01-31)
  • Mmm - Three-letter month abbreviation (Jan-Dec)
  • yyyy - Four-digit year
  • HH:MM:SS - 24-hour time format
  • +0000 - Timezone offset from UTC

PHP Generation:

php
1// Current time in UTC +3
2$date = new DateTime( 'now', new DateTimeZone( 'Europe/Moscow' ) );
3echo $date->format( 'D, d M Y H:i:s O' ); // Mon, 06 Sep 2024 03:01:00 +0300
4
5// From post date
6$post_date = get_the_time( 'U' ); // Unix timestamp
7echo date( 'D, d M Y H:i:s O', $post_date ); // Correctly formatted

Enclosure Element for Media Distribution

The <enclosure> element enables podcast and media distribution:

xml
1<enclosure
2 url="https://example.com/media.mp3"
3 length="12345678"
4 type="audio/mpeg" />

Attributes:

  • url - Valid HTTP/HTTPS URL (required)
  • length - File size in bytes (required, can be 0)
  • type - MIME type (required)

Common MIME Types:

  • audio/mpeg - MP3 audio
  • audio/wav - WAV audio
  • audio/x-m4a - M4A audio
  • video/mp4 - MP4 video
  • video/quicktime - MOV video
  • image/jpeg - JPEG image
  • image/png - PNG image
  • application/pdf - PDF document

PHP Implementation:

php
1function add_media_enclosures() {
2 add_action( 'rss2_item', function() {
3 global $post;
4
5 // Add featured image
6 if ( has_post_thumbnail() ) {
7 $image_url = get_the_post_thumbnail_url( $post->ID, 'full' );
8 $image_id = get_post_thumbnail_id( $post->ID );
9 $image_meta = wp_get_attachment_metadata( $image_id );
10
11 echo '<enclosure ';
12 echo 'url="' . esc_url( $image_url ) . '" ';
13 echo 'length="' . $image_meta['filesize'] . '" ';
14 echo 'type="image/jpeg" />' . "\n";
15 }
16
17 // Add attached files
18 $attachments = get_attached_media( '', $post->ID );
19 foreach ( $attachments as $attachment ) {
20 $file_url = wp_get_attachment_url( $attachment->ID );
21 $file_meta = wp_get_attachment_metadata( $attachment->ID );
22
23 echo '<enclosure ';
24 echo 'url="' . esc_url( $file_url ) . '" ';
25 echo 'length="' . $file_meta['filesize'] . '" ';
26 echo 'type="' . $attachment->post_mime_type . '" />' . "\n";
27 }
28 });
29}
30
31add_action( 'init', 'add_media_enclosures' );

GUID (Global Unique Identifier) Best Practices

The <guid> element uniquely identifies items across all feeds:

php
1// Correct: Use post permalink with isPermaLink="true"
2<guid isPermaLink="true"><?php the_permalink_rss(); ?></guid>
3
4// Correct: Use permanent identifier with isPermaLink="false"
5<guid isPermaLink="false"><?php echo get_the_guid(); ?></guid>
6
7// Best practice for stability: Use unique identifier
8<guid isPermaLink="false">
9 <?php echo home_url( '?p=' . get_the_ID() ); ?>
10</guid>

GUID Guidelines:

  • isPermaLink="true" - GUID must be valid HTTP URL
  • isPermaLink="false" - GUID is permanent unique string
  • Never change GUID after publication (breaking change for aggregators)
  • Use consistent format across feed lifetime

Empty or Missing Elements

RSS readers handle empty elements differently:

php
1// ❌ Leave tags empty - may be ignored by some readers
2<description></description>
3
4// ✅ Use CDATA with placeholder
5<description><![CDATA[No description available]]></description>
6
7// ✅ Omit element if no data
8// (if optional and empty)
9
10// ✅ Set sensible default
11<description><?php echo wp_trim_words( get_the_excerpt(), 50 ); ?></description>

Advanced Caching Strategies

Multi-Level Cache with Fallback

php
1function generate_custom_feed_with_cascade_cache() {
2 $cache_key = 'custom_feed_output';
3 $cache_group = 'feeds';
4
5 // Check object cache first (in-memory, fastest)
6 $cached_feed = wp_cache_get( $cache_key, $cache_group );
7
8 if ( false === $cached_feed ) {
9 // Check transients (database-backed, persistent)
10 $cached_feed = get_transient( $cache_key );
11 }
12
13 if ( $cached_feed ) {
14 echo $cached_feed;
15 exit;
16 }
17
18 // Generate fresh feed
19 ob_start();
20 get_template_part( 'custom' );
21 $feed_output = ob_get_clean();
22
23 // Store in both caches
24 wp_cache_set( $cache_key, $feed_output, $cache_group, HOUR_IN_SECONDS );
25 set_transient( $cache_key, $feed_output, HOUR_IN_SECONDS );
26
27 echo $feed_output;
28}

Cache Hierarchy:

  1. Object Cache - In-memory (Redis/Memcached if configured)
  2. Transients - Database (with automatic expiration)
  3. File Cache - File system (requires custom implementation)
  4. HTTP Headers - Browser/CDN cache headers

Cache Invalidation on Content Changes

php
1function invalidate_feed_cache_on_updates() {
2 // Clear on post publish/update
3 add_action( 'save_post', function( $post_id ) {
4 wp_cache_delete( 'custom_feed_output', 'feeds' );
5 delete_transient( 'custom_feed_output' );
6 });
7
8 // Clear on category updates
9 add_action( 'edited_category', function() {
10 wp_cache_delete( 'custom_feed_output', 'feeds' );
11 delete_transient( 'custom_feed_output' );
12 });
13
14 // Clear on plugin deactivation
15 add_action( 'deactivated_plugin', function() {
16 wp_cache_flush();
17 });
18
19 // Clear on theme switch
20 add_action( 'switch_theme', function() {
21 wp_cache_flush();
22 });
23}
24
25add_action( 'init', 'invalidate_feed_cache_on_updates' );

Security Considerations for RSS Feeds

Escaping and Sanitization

Protect against XSS attacks in feed content:

php
1// ❌ Dangerous - allows script injection
2<title><?php echo get_the_title(); ?></title>
3
4// ✅ Correct - escapes HTML
5<title><?php echo esc_html( get_the_title() ); ?></title>
6
7// ✅ Better for URLs
8<link><?php echo esc_url( the_permalink() ); ?></link>
9
10// ✅ For CDATA content with HTML
11<description><![CDATA[
12 <?php
13 echo wp_kses_post( apply_filters( 'the_content', get_the_content() ) );
14 ?>
15]]></description>

Sensitive Data Filtering

Prevent exposure of private information:

php
1function sanitize_feed_content() {
2 add_filter( 'the_content', function( $content ) {
3 if ( is_feed( 'custom' ) ) {
4 // Remove password-protected post markers
5 $content = preg_replace( '/\[password-protected\]/', '', $content );
6
7 // Remove admin-only content
8 $content = preg_replace( '/\[admin-only\].*?\[\/admin-only\]/s', '', $content );
9
10 // Remove internal notes
11 $content = preg_replace( '/<!--\s*NOTE:.*?-->/', '', $content );
12 }
13 return $content;
14 });
15}
16
17add_action( 'init', 'sanitize_feed_content' );

HTTPS Enforcement

Ensure all feed URLs use HTTPS:

php
1add_filter( 'post_type_archive_feed_link', function( $feed_link ) {
2 return set_url_scheme( $feed_link, 'https' );
3});
4
5add_filter( 'post_comments_feed_link', function( $feed_link ) {
6 return set_url_scheme( $feed_link, 'https' );
7});