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:
- Dynamic Content Loading: Uses
WP_Queryto fetch posts with customizable parameters - Metadata Extraction: Retrieves post data (title, permalink, excerpt, content, categories)
- Image Enclosures: Includes featured images as media enclosures for RSS readers supporting multimedia
- RFC 2822 Date Format: Publishes dates in the format required by RSS 2.0 specification (RFC 2822)
- 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_queryfor 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.
1<?php2header('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 <?php15 $args = array(16 'post_type' => 'post', // Fetch posts or any custom post types17 'posts_per_page' => 10 // Limit the number of posts18 );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 <?php33 // Get post categories34 $categories = get_the_category();35 if (!empty($categories)):36 foreach ($categories as $category):37 ?>38 <category><?php echo $category->name; ?></category>39 <?php40 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 URL51 ?>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 <?php57 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:
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:
inithook (priority 10): Feed types are registered during WordPress initialization- WordPress processes rewrite rules and matches feed requests
- Callback function executes to output feed content
Why get_template_part('custom')?
- Locates the
custom.phptemplate 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
-
Initialization Phase (
inithook, priority 10)add_feed()registers feed type with rewrite rules- WordPress core feeds (
feed,rss,rss2,atom,rdf) are available by default
-
Request Parsing Phase (
parse_requesthook)- WordPress matches URL against registered feed patterns
- Feed query variable (
wp->query_vars['feed']) is populated - Example:
yoursite.com/feed/custom/setsquery_vars['feed'] = 'custom'
-
Template Loading Phase
- WordPress locates template file via
get_template_part() - Child theme
custom.phpchecked first, then parent theme - Falls back to theme root if custom.php not found
- WordPress locates template file via
-
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-Typeheader
WordPress Core Feed Hooks
Built-in hooks available for all feed types:
1// Fires before feed content is generated2do_action( 'feed_head' );3
4// Fires for each item in RSS/Atom feeds5do_action( 'rss2_item' );6do_action( 'atom_entry' );7
8// Fires at end of feed generation9do_action( 'feed_tail' );Feed-Specific Query Hooks
Customize feed queries using these filters:
1// Modify WHERE clause for feed queries2apply_filters( 'posts_where_paged', $where, $query );3
4// Modify JOIN clause5apply_filters( 'posts_join_paged', $join, $query );6
7// Modify ORDER BY clause8apply_filters( 'posts_orderby', $orderby, $query );9
10// Modify GROUP BY clause11apply_filters( 'posts_groupby', $groupby, $query );12
13// Skip filters entirely for direct database queries14'suppress_filters' => trueMethod 1: Query Variable Interception
The following approach intercepts WordPress query variables at parse time:
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
Method 2: Rewrite Rules Approach (Recommended)
Using add_rewrite_rule() provides cleaner URL structures:
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 impactURL 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:
1function generate_custom_feed() {2 if (isset($_GET['type']) && $_GET['type'] === 'full') {3 get_template_part('custom-full'); // Full content feed4 } elseif (isset($_GET['type']) && $_GET['type'] === 'excerpt') {5 get_template_part('custom-excerpt'); // Excerpt-only feed6 } else {7 get_template_part('custom'); // Default feed8 }9}10
11// Access via: yoursite.com/feed/custom/?type=fullPerformance Optimization Techniques
Caching RSS Output
RSS feeds are frequently accessed by aggregators. Implementing caching reduces database queries:
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 updates19add_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:
1$args = array(2 'post_type' => 'post',3 'posts_per_page' => 20, // Limit to 20 items4 'orderby' => 'modified', // Sort by last modified5 'order' => 'DESC', // Most recent first6 'post_status' => 'publish', // Only published posts7 'no_found_rows' => true, // Skip total row count (faster)8 'update_post_meta_cache' => false, // Skip unnecessary metadata loading9 'update_post_term_cache' => false, // Skip term cache10);XML Validation and Best Practices
XML Declaration and Encoding
Always specify encoding explicitly:
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:
1// ❌ Incorrect - XML parsing fails2<description><?php echo $content; ?></description>3
4// ✅ Correct - Escapes all special characters5<description><?php echo esc_html($content); ?></description>6
7// ✅ Best - Uses CDATA for formatted content8<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
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" category9 ),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:
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:
1$query = new WP_Query( $args );2// Internal flow:3// 1. __construct() called4// 2. query() method invoked5// 3. parse_query() processes arguments6// 4. Query filters applied (if suppress_filters is false)7// 5. get_posts() executes SQL query8// 6. Results stored in $query->postsEssential Query Parameters for Feed Generation
Fundamental Parameters:
1$args = array(2 // ========== CONTENT SELECTION ==========3 'post_type' => array( 'post', 'page' ), // Multiple types supported4 'post_status' => 'publish', // Only published posts5 'posts_per_page' => 20, // Feed item limit6
7 // ========== ORDERING AND PAGINATION ==========8 'orderby' => 'modified', // Sort by last modified date9 'order' => 'DESC', // Most recent first10 'paged' => 1, // Page number for pagination11
12 // ========== PERFORMANCE OPTIMIZATION ==========13 'no_found_rows' => true, // Skip counting total rows (faster)14 'suppress_filters' => false, // Apply standard filters15 '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 displayedupdate_post_term_cache: false- Avoids taxonomy loading if not displayed
Filter by Post Meta
1$args = array(2 'post_type' => 'post',3 'posts_per_page' => 10,4 'meta_query' => array(5 array(6 'key' => '_featured', // Custom meta key7 'value' => '1',8 'compare' => '=',9 ),10 ),11);Meta Query Comparison Operators:
1$args = array(2 'post_type' => 'product',3 'meta_query' => array(4 // Exact match5 array(6 'key' => 'price',7 'value' => '100',8 'compare' => '=',9 'type' => 'numeric',10 ),11
12 // Range queries13 array(14 'key' => 'price',15 'value' => array( 20, 100 ),16 'compare' => 'BETWEEN',17 'type' => 'numeric',18 ),19
20 // Pattern matching21 array(22 'key' => 'color',23 'value' => 'blue',24 'compare' => 'LIKE',25 ),26
27 // NOT LIKE pattern28 array(29 'key' => 'color',30 'value' => 'blue',31 'compare' => 'NOT LIKE',32 ),33
34 // Field existence check35 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 truemeta_compare: 'OR'- At least one condition must be true
Exclude Posts
1$args = array(2 'post_type' => 'post',3 'posts_per_page' => 10,4 'post__not_in' => array(1, 2, 3), // Exclude specific post IDs5);Advanced Exclusion Patterns:
1$args = array(2 'post_type' => 'post',3
4 // Exclude specific posts5 'post__not_in' => array(1, 2, 3),6
7 // Include only specific posts8 'post__in' => array(5, 10, 15),9
10 // Exclude sticky posts11 'post__not_in' => get_option( 'sticky_posts' ),12
13 // Exclude author14 'author__not_in' => array(1, 2),15
16 // Exclude taxonomy terms17 'tax_query' => array(18 array(19 'taxonomy' => 'category',20 'field' => 'id',21 'terms' => array(1, 2, 3),22 'operator' => 'NOT IN', // Exclude these categories23 ),24 ),25);Advanced Taxonomy Filtering
1$args = array(2 'post_type' => 'post',3 'tax_query' => array(4 'relation' => 'AND', // Combine multiple taxonomy conditions5 array(6 'taxonomy' => 'category',7 'field' => 'slug',8 'terms' => array( 'technology', 'science' ),9 'operator' => 'IN', // Match any term10 ),11 array(12 'taxonomy' => 'post_tag',13 'field' => 'id',14 'terms' => array( 5, 10 ),15 'operator' => 'NOT IN', // Exclude these tags16 ),17 ),18);Taxonomy Query Operators:
IN- Match any of the termsNOT IN- Exclude these termsAND- Match all terms
Search and Text Filtering
1$args = array(2 'post_type' => 'post',3 's' => 'keyword', // Full-text search4 'sentence' => true, // Search exact phrase5);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
1$args = array(2 'post_type' => 'post',3 'year' => 2024, // Posts from 20244 'monthnum' => 12, // December5 'day' => 25, // 25th day6 'w' => 1, // Week number7 '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:
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:
1$query = new WP_Query( array( 's' => 'keyword' ) );This searches in:
post_title- Post title fieldpost_content- Main content area- Can be extended via filters
Display Post by Slug using WP_Query
Fetch content using URL-friendly identifiers:
1$query = new WP_Query( array( 'name' => 'about-my-life' ) );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:
1define('WP_DEBUG', true);2define('WP_DEBUG_LOG', true);3define('WP_DEBUG_DISPLAY', false); // Don't display errors on frontendFeed debug messages appear in /wp-content/debug.log.
Verify Feed Generation
Check raw XML output:
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
1// Access comment feed for specific post2/feed/comment-on/post-id/3
4// Access comment feed for all posts5/feed/comments/6
7// Access comment feed in Atom format8/feed/comments/atom/Comment Feed Hooks
Built-in hooks fired during comment feed generation:
1// Fires at the end of each comment RSS2 item2do_action( 'commentrss2_item', $comment_id, $comment_post_id );3
4// Fires inside feed tag in Atom comment feed5do_action( 'atom_comments_ns' );6
7// Fires at end of Atom comment entry8do_action( 'atom_entry' );Comment Feed Query Filters
Customize comment feed queries at database level:
1// Filter WHERE clause for comment feeds2apply_filters( 'comment_feed_where', $where );3
4// Filter JOIN clause5apply_filters( 'comment_feed_join', $join );6
7// Filter ORDER BY clause8apply_filters( 'comment_feed_orderby', $orderby );9
10// Filter GROUP BY clause11apply_filters( 'comment_feed_groupby', $groupby );12
13// Filter LIMIT clause14apply_filters( 'comment_feed_limits', $limits );Custom Comment Feed Example
1function customize_comment_feed() {2 add_filter( 'comment_feed_where', function( $where ) {3 global $wpdb;4
5 // Exclude comment spam6 $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 first15 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:
1add_filter( 'default_feed', function( $feed ) {2 return 'rss2'; // Use RSS 2.0 by default (default is 'rss')3});Custom Feed Link Filters
Intercept and modify feed URLs generated by WordPress:
1// Modify post comments feed permalink2add_filter( 'post_comments_feed_link', function( $feed_link, $post_id ) {3 // Example: Use custom domain4 return str_replace( 'example.com', 'feeds.example.com', $feed_link );5}, 10, 2 );6
7// Modify post type archive feed link8add_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:
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
1add_action( 'rss2_item', function() {2 global $post;3
4 // Add custom metadata before item closes5 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:
1// Add to wp-config.php2define( '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:
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 data9foreach ( $query->posts as $post_id ) {10 $post = get_post( $post_id );11 // Load only needed meta fields12 $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
1add_filter( 'posts_join', function( $join, $query ) {2 if ( $query->is_feed() ) {3 global $wpdb;4
5 // Only join postmeta if querying by meta6 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:
1-- Primary key for fast post retrieval2INDEX wp_posts_post_status_post_type_post_date (post_status, post_type, post_date)3
4-- For meta queries5INDEX wp_postmeta_post_id_meta_key (post_id, meta_key)6
7-- For taxonomy queries8INDEX wp_term_relationships_term_id (term_id)9INDEX wp_term_relationships_term_taxonomy_id (term_taxonomy_id)10
11-- For comment queries12INDEX 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 yearHH:MM:SS- 24-hour time format+0000- Timezone offset from UTC
PHP Generation:
1// Current time in UTC +32$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 +03004
5// From post date6$post_date = get_the_time( 'U' ); // Unix timestamp7echo date( 'D, d M Y H:i:s O', $post_date ); // Correctly formattedEnclosure Element for Media Distribution
The <enclosure> element enables podcast and media distribution:
1<enclosure2 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 audioaudio/wav- WAV audioaudio/x-m4a- M4A audiovideo/mp4- MP4 videovideo/quicktime- MOV videoimage/jpeg- JPEG imageimage/png- PNG imageapplication/pdf- PDF document
PHP Implementation:
1function add_media_enclosures() {2 add_action( 'rss2_item', function() {3 global $post;4
5 // Add featured image6 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 files18 $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:
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 identifier8<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:
1// ❌ Leave tags empty - may be ignored by some readers2<description></description>3
4// ✅ Use CDATA with placeholder5<description><![CDATA[No description available]]></description>6
7// ✅ Omit element if no data8// (if optional and empty)9
10// ✅ Set sensible default11<description><?php echo wp_trim_words( get_the_excerpt(), 50 ); ?></description>Advanced Caching Strategies
Multi-Level Cache with Fallback
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 feed19 ob_start();20 get_template_part( 'custom' );21 $feed_output = ob_get_clean();22
23 // Store in both caches24 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:
- Object Cache - In-memory (Redis/Memcached if configured)
- Transients - Database (with automatic expiration)
- File Cache - File system (requires custom implementation)
- HTTP Headers - Browser/CDN cache headers
Cache Invalidation on Content Changes
1function invalidate_feed_cache_on_updates() {2 // Clear on post publish/update3 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 updates9 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 deactivation15 add_action( 'deactivated_plugin', function() {16 wp_cache_flush();17 });18
19 // Clear on theme switch20 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:
1// ❌ Dangerous - allows script injection2<title><?php echo get_the_title(); ?></title>3
4// ✅ Correct - escapes HTML5<title><?php echo esc_html( get_the_title() ); ?></title>6
7// ✅ Better for URLs8<link><?php echo esc_url( the_permalink() ); ?></link>9
10// ✅ For CDATA content with HTML11<description><![CDATA[12 <?php13 echo wp_kses_post( apply_filters( 'the_content', get_the_content() ) );14 ?>15]]></description>Sensitive Data Filtering
Prevent exposure of private information:
1function sanitize_feed_content() {2 add_filter( 'the_content', function( $content ) {3 if ( is_feed( 'custom' ) ) {4 // Remove password-protected post markers5 $content = preg_replace( '/\[password-protected\]/', '', $content );6
7 // Remove admin-only content8 $content = preg_replace( '/\[admin-only\].*?\[\/admin-only\]/s', '', $content );9
10 // Remove internal notes11 $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:
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});