Allow users to create their own feed from selected categories?

I’ve been wandering around on the web for the last couple of hours wondering if there’s a way to allow users to build their own RSS feed by selecting categories within WordPress, that could then be subscribed to by email. Seems to present two problems:

  1. Allowing people to build a personalized feed from categories.
  2. Enabling email subscription.

Any thoughts on how best to proceed with either?

Related posts

Leave a Reply

4 comments

  1. This is a really cool idea.

    I don’t think part 2 should be handled inside WordPress: there are plenty of RSS to email providers. They’re going to be way better at that than a plugin (or theme) is likely to be.

    BUT we can create RSS feeds.

    Step one: set up a class to wrap everything up.

    There are a few class constants and variables here — we’ll use them later. Just a singleton pattern.

    <?php
    class Per_User_Feeds
    {
        // Where we'll store the user cats
        const META_KEY = '_per_user_feeds_cats';
    
        // Nonce for the form fields
        const NONCE = '_user_user_feeds_nonce';
    
        // Taxonomy to use
        const TAX = 'category';
    
        // The query variable for the rewrite
        const Q_VAR = 'puf_feed';
    
        // container for the instance of this class
        private static $ins = null;
    
        // container for the terms allowed for this plugin
        private static $terms = null;
    
        public static function init()
        {
            add_action('plugins_loaded', array(__CLASS__, 'instance'));
        }
    
        public static function instance()
        {
            is_null(self::$ins) && self::$ins = new self;
            return self::$ins;
        }
    }
    

    Step two: add a field to the user profile pages (and save it)

    You’ll need to hook into show_user_profile and edit_user_profile to do this. Spit out a nonce, a label, and the field. show_user_profile fires when users view their profile in the admin area. edit_user_profile fires when they edit another’s profile — this is how your admin user will go in an edit user’s categories.

    <?php
    class Per_User_Feeds
    {
        // snip snip
    
        protected function __construct()
        {
            add_action('show_user_profile', array($this, 'field'));
            add_action('edit_user_profile', array($this, 'field'));
        }
    
        public function field($user)
        {
            wp_nonce_field(self::NONCE . $user->ID, self::NONCE, false);
    
            echo '<h4>', esc_html__('Feed Categories', 'per-user-feed'), '</h4>';
    
            if($terms = self::get_terms())
            {
                $val = self::get_user_terms($user->ID);
                printf('<select name="%1$s[]" id="%1$s" multiple="multiple">', esc_attr(self::META_KEY));
                echo '<option value="">', esc_html__('None', 'per-user-feed'), '</option>';
                foreach($terms as $t)
                {
                    printf(
                        '<option value="%1$s" %3$s>%2$s</option>',
                        esc_attr($t->term_id),
                        esc_html($t->name),
                        in_array($t->term_id, $val) ? 'selected="selected"' : ''
                    );
                }
                echo '</select>';
            }
        }
    }
    

    That also introduces our first two helper methods:

    1. get_user_terms, a simple wraper around get_user_meta with a call to apply_filters — let others modify things if they want!
    2. get_terms a wrapper around get_terms with a call to apply_filters.

    Both of these are just convenience things. They also provide ways for other plugins/themes to hook in and modify things.

    <?php
    /**
     * Get the categories available for use with this plugin.
     *
     * @uses    get_terms
     * @uses    apply_filters
     * @return  array The categories for use
     */
    public static function get_terms()
    {
        if(is_null(self::$terms))
            self::$terms = get_terms(self::TAX, array('hide_empty' => false));
    
        return apply_filters('per_user_feeds_terms', self::$terms);
    }
    
    /**
     * Get the feed terms for a given user.
     *
     * @param   int $user_id The user for which to fetch terms
     * @uses    get_user_meta
     * @uses    apply_filters
     * @return  mixed The array of allowed term IDs or an empty string
     */
    public static function get_user_terms($user_id)
    {
        return apply_filters('per_user_feeds_user_terms',
            get_user_meta($user_id, self::META_KEY, true), $user_id);
    }
    

    To save the fields, hook into personal_options_update (fires when user save their own profile) and edit_user_profile_update (fires when saving another user’s profile).

    <?php
    class Per_User_Feeds
    {
        // snip snip
    
        protected function __construct()
        {
            add_action('show_user_profile', array($this, 'field'));
            add_action('edit_user_profile', array($this, 'field'));
            add_action('personal_options_update', array($this, 'save'));
            add_action('edit_user_profile_update', array($this, 'save'));
        }
    
        // snip snip
    
        public function save($user_id)
        {
            if(
                !isset($_POST[self::NONCE]) ||
                !wp_verify_nonce($_POST[self::NONCE], self::NONCE . $user_id)
            ) return;
    
            if(!current_user_can('edit_user', $user_id))
                return;
    
            if(!empty($_POST[self::META_KEY]))
            {
                $allowed = array_map(function($t) {
                    return $t->term_id;
                }, self::get_terms());
    
                // PHP > 5.3: Make sure the items are in our allowed terms.
                $res = array_filter(
                    (array)$_POST[self::META_KEY],
                    function($i) use ($allowed) {
                        return in_array($i, $allowed);
                    }
                );
    
                update_user_meta($user_id, self::META_KEY, array_map('absint', $res));
            }
            else
            {
                delete_user_meta($user_id, self::META_KEY);
            }
        }
    }
    

    Step three: provide a feed

    Since this is very much a custom feed, we don’t want to hijack something like author feeds to get this done (though that’s an option!). Instead let’s add a rewrite: yoursite.com/user-feed/{{user_id}} will render the personalized user feed.

    To add the rewrite we need to hook into init and use add_rewrite_rule. Since this uses a custom query variable to detect when we’re on a personalized user feed, we also need to hook into query_vars and our our custom variable so WordPress doesn’t ignore it.

    <?php
    class Per_User_Feeds
    {
        // snip snip
    
        protected function __construct()
        {
            add_action('show_user_profile', array($this, 'field'));
            add_action('edit_user_profile', array($this, 'field'));
            add_action('personal_options_update', array($this, 'save'));
            add_action('edit_user_profile_update', array($this, 'save'));
            add_action('init', array($this, 'rewrite'));
            add_filter('query_vars', array($this, 'query_var'));
        }
    
        // snip snip
    
        public function rewrite()
        {
            add_rewrite_rule(
                '^user-feed/(d+)/?$',
                'index.php?' . self::Q_VAR . '=$matches[1]',
                'top'
            );
        }
    
        public function query_var($v)
        {
            $v[] = self::Q_VAR;
            return $v;
        }
    }
    

    To actually render the feed, we’ll hook into template_redirect, look for our custom query var (bailing if we don’t find it), and hijack the global $wp_query with a personalized version.

    I also hooked into wp_title_rss to modify the RSS title, which was a bit weird: it grabbed the first category and displayed the feed title as if looking at a single category.

    <?php
    class Per_User_Feeds
    {
        // snip snip
    
        protected function __construct()
        {
            add_action('show_user_profile', array($this, 'field'));
            add_action('edit_user_profile', array($this, 'field'));
            add_action('personal_options_update', array($this, 'save'));
            add_action('edit_user_profile_update', array($this, 'save'));
            add_action('init', array($this, 'rewrite'));
            add_filter('query_vars', array($this, 'query_var'));
            add_action('template_redirect', array($this, 'catch_feed'));
        }
    
        // snip snip
    
        public function catch_feed()
        {
            $user_id = get_query_var(self::Q_VAR);
    
            if(!$user_id)
                return;
    
            if($q = self::get_user_query($user_id))
            {
                global $wp_query;
                $wp_query = $q;
    
                // kind of lame: anon function on a filter...
                add_filter('wp_title_rss', function($title) use ($user_id) {
                    $title = ' - ' . __('User Feed', 'per-user-feed');
    
                    if($user = get_user_by('id', $user_id))
                        $title .= ': ' . $user->display_name;
    
                    return $title;
                });
            }
    
            // maybe want to handle the "else" here?
    
            // see do_feed_rss2
            load_template( ABSPATH . WPINC . '/feed-rss2.php' );
            exit;
        }
    }
    

    To actually render the feed we rely on wp-includes/feed-rss2.php. You could replace this with something more custom, but why not be lazy?

    There’s also a third helper method here: get_user_query. Same idea as the helpers above — abstract away some reusable functionality and provide hooks.

    <?php
    /**
     * Get a WP_Query object for a given user.
     *
     * @acces   public
     * @uses    WP_Query
     * @return  object WP_Query
     */
    public static function get_user_query($user_id)
    {
        $terms = self::get_user_terms($user_id);
    
        if(!$terms)
            return apply_filters('per_user_feeds_query_args', false, $terms, $user_id);
    
        $args = apply_filters('per_user_feeds_query_args', array(
            'tax_query' => array(
                array(
                    'taxonomy'  => self::TAX,
                    'terms'     => $terms,
                    'field'     => 'id',
                    'operator'  => 'IN',
                ),
            ),
        ), $terms, $user_id);
    
        return new WP_Query($args);
    }
    

    Here is all of the above as a plugin. The plugin (and subsequently, this answer) requires PHP 5.3+ due to the use of anonymous functions.

  2. I do this using the regular WordPress Category Feeds and MailChimp to provide my email subscribers the option of receiving new posts only for categories they’re interested in receiving.

    Within MailChimp, you create a Group for each WordPress category and then on your email subscription form you allow your subscribers to select the groups (i.e., categories) they’re interested in subscribing to (a set of checkboxes is probably easiest). When they subscribe, their selections will be passed along and they will be put into those groups on MailChimp.

    Then on MailChimp you create an RSS Campaign for each category using the Category Feed and specify in the campaign settings to only send new posts to a segment of your subscribers (the segment that has selected the group corresponding to that category).

  3. The simplest would be to add a series of two very short (mu-)plugins. This also adds routes for page/2, etc.:

    http://example.com/u/%author%

    <?php
    /** Plugin Name: (WPSE) #46074 Add /u/%author% routes */
    
    register_activation_hook(   __FILE__, function() { flush_rewrite_rules(); } );
    register_deactivation_hook( __FILE__, function() { flush_rewrite_rules(); } );
    
    add_action( 'init', function()
    {
        // Adds `/u/{$author_name}` routes
        add_rewrite_rule(
            'u/([^/]+)/?',
            'index.php?author_name=$matches[1]',
            'top'
        );
        add_rewrite_rule(
            'u/([^/]+)/page/?([0-9]{1,})/?',
            'index.php?author_name=$matches[1]&paged=$matches[2]',
            'top'
        );
    }
    

    http://example.com/p/%postname%

    <?php
    /** Plugin Name: (WPSE) #46074 Add /u/%author% routes */
    
    register_activation_hook(   __FILE__, function() { flush_rewrite_rules(); } );
    register_deactivation_hook( __FILE__, function() { flush_rewrite_rules(); } );
    
    add_action( 'init', function()
    {
        // Adds `/p/{$postname}` routes
        add_rewrite_rule(
            'p/([^/]+)/?',
            'index.php?p=$matches[1]',
            'top'
        );
        add_rewrite_rule(
            'p/([^/]+)/page/?([0-9]{1,})/?',
            'index.php?p=$matches[1]&paged=$matches[2]',
            'top'
        );
    }
    
  4. WordPress already supplies RSS feeds for each category, this is the codex article explaining how they’re structured:

    http://codex.wordpress.org/WordPress_Feeds#Categories_and_Tags

    To get email subscription working I usually set up Feedburner with email subscription enabled (after claiming a feed, go to Publicize > Email subscriptions). That would require you to take your category feeds and set each one up in Feedburner, then add those links to your site in appropriate places. If you’re dealing with a ton of categories that might be quite a bit of work. Hopefully other people here will have suggestions.

    Best of luck!