WordPress challenge of the day: customizing The Events Calendar’s RSS feed and how it displays in a MailChimp RSS campaign

This one’s a doozy. I have a client who is using The Events Calendar, and they want to automate a weekly email blast listing that week’s events, using MailChimp.

The Events Calendar automatically generates an RSS feed of future events, inserting the event’s date and time in the RSS <pubDate> field. And MailChimp offers an RSS Campaign feature that can be scheduled to automatically send out emails with content pulled in from an RSS feed.

So far so good. But there were a few things the client wanted that were missing:

  1. Show exactly a week’s worth of events. The RSS feed just pulls in n events… whatever you have set in Settings > Reading > Syndication feeds show the most recent.
  2. Display the event’s featured image. Featured images aren’t included in WordPress RSS feeds, neither the default posts feed nor The Events Calendar’s modified feed.
  3. Show the event’s location. This is also not pulled into the RSS feed at all.

To make this happen, I had to first get the RSS feed to actually contain the right data. Then I had to modify the MailChimp campaign to display the information.

The problem in both cases surrounded documentation. RSS, though it’s still widely used, is definitely languishing if not dead. The spec is well-defined, but there’s not a lot of good information about how you can customize the WordPress RSS feed, and even less about how to customize The Event Calendar’s version. What info I could find was generally outdated or flat-out wrong — like the example in the official WordPress documentation (the old documentation, to be fair) that has at least three major errors in it. (I’m not even going to bother to explain them. Just trust that it’s wrong and you shouldn’t use it.)

Now that I’ve put in the hours of trial and error and futile Googling, I’ll save you the trouble and summarize my successful end result.

Problem 1: Show a week’s worth of events in the RSS feed

It took a surprising amount of effort to figure out how this is done, although in the end it’s a very small amount of code. Part of the problem was that I was not aware of the posts_per_rss query parameter, and therefore I wasted a lot of time trying to figure out why posts_per_page wasn’t working. Maybe that’s just my dumb mistake. I hope so.

I also spent a bunch of time trying to get a meta_query working before I realized that The Events Calendar adds an end_date query parameter which makes it super-easy to define a date-based endpoint for the query.

You need both of these. Depending on how full your calendar is, the default posts_per_rss value of 10 is possibly not enough to cover a full week. I decided to change it to 100. If this client ever has a week with more than 100 events in it, we’ll be in trouble… probably in more ways than one.

Here’s the modification you need. Put this in your functions.php file or wherever you feel is appropriate:

// Modify feed query
function my_rss_pre_get_posts($query) {
  if ($query->is_feed() && $query->tribe_is_event_query) {
    // Change number of posts retrieved on events feed
    $query->set('posts_per_rss', 100);
    // Add restriction to only show events within one week
    $query->set('end_date', date('Y-m-d H:i:s', mktime(23, 59, 59, date('n'), date('j') + 7, date('Y'))));
  return $query;

What’s happening here? The if conditional is critical, since pre_get_posts runs on… oh… every database query. This makes sure it’s only running on a query to retrieve the RSS feed and, specifically, The Events Calendar’s events query.

We’re changing posts_per_rss to an arbitrarily large value — the maximum number of events we can possibly anticipate having within the date range we’re about to set.

The change to end_date (it’s actually empty by default) sets a maximum event end date to retrieve. My mktime function call is setting the date to 11:59:59 pm on the date one week from the current date. You can just change the 7 to another number to set the query to that many days in the future. There are a lot of other fun manipulations you can make to mktime. Check out the official PHP documentation if you’re unfamiliar with it.

Every add_action() call can include priority as the third input parameter. Sometimes it doesn’t matter and you can leave it blank, but in this case it does matter. I’m not sure what the minimum value is that would work, but I found 99 does, so I stuck with that.

Problems 2 and 3: Add the featured image and event location to the RSS feed

RSS is XML, so it has a syntax similar to HTML, but with its own specific tags. (And with XML’s much stricter validation requirements.) WordPress uses RSS 2.0. This can get you into trouble later with the MailChimp integration, because MailChimp’s RSS Merge Tags documentation gives an example of the RSS 1.5 <media:content> tag for inserting images, but you’ll actually need to use the <enclosure> tag… which MailChimp also mentions, but not in conjunction with images. Still with me?

All right, so the first thing we’re going to need to modify in the RSS output is the images. And don’t believe that official WordPress documentation I mentioned earlier. It. Is. Wrong. My way works.

The next thing we want to do, and we’ll roll it into the same function (because I want to contain the madness), is to add in the event’s location. There’s no RSS tag to account for something like this. You could add it to the <description> tag, although I found that since the WordPress rss2_item hook seems to be directly outputting RSS XML as it goes, I didn’t track down a way to modify any of the output, just add to it.

There’s another standard RSS tag that WordPress doesn’t use — or at least doesn’t seem to use — the <source> tag. This is supposed to be used to provide a link and title of an external reference for the item, but I’m going to take the liberty of misusing it to pass along the location name instead. In my particular case I’m not using it as a link; I just need the text of the location name. But the url attribute is required, so I just stuck the event’s URL in there. (I also added a conditional so this is only inserted on events, not on other post types. But for images I figured it would be a nice bonus to add the featured image across all post types on the site. You may want to add your own conditionals to limit this.)

Here we go:

function my_rss_modify_item() {
  global $post;
  // Add featured image
  $uploads = wp_upload_dir();
  if (has_post_thumbnail($post->ID)) {
    $thumbnail = wp_get_attachment_image_src(get_post_thumbnail_id($post->ID), 'thumbnail');
    $image = $thumbnail[0];
    $ext = pathinfo($image, PATHINFO_EXTENSION);
    $mime = ($ext == 'jpg') ? 'image/jpeg' : 'image/' . $ext;
    $path = $uploads['basedir'] . substr($image, (strpos($image, '/uploads') + strlen('/uploads')));
    $size = filesize($path);
    echo '<enclosure url="' . esc_url($image) . '" length="' . intval($size) . '" type="' . esc_attr($mime) . '" />' . "\n";
  // Add event location (fudged into the <source> tag)
  if ($post->post_type == 'tribe_events') {
    if ($location = strip_tags(tribe_get_venue($post->ID))) {
      echo '<source url="' . get_permalink($post->ID) . '">' . $location . '</source>';

You might be able to find a more efficient way of obtaining the $path value… to be honest I was getting a bit fatigued by this point in the process! But it works. You really only need that value anyway in order to fill in the length attribute, and apparently that value doesn’t even need to be correct, it just needs to be there for the XML to validate. So maybe you can try leaving it out entirely.

Put it in MailChimp!

OK… I’m not going to tell you how to set up an RSS Campaign in MailChimp. I already linked to their docs. But I will tell you how to customize the template to include these nice new features you’ve added to your RSS feed.

Edit the campaign, and once you’re in the Campaign Builder, place an RSS Items block, then click on it to open the editor on the right side. Set the dropdown to Custom, which will reveal a WYSIWYG editor full of a bunch of special tags that dynamically insert RSS content into the layout. For the most part you can edit everything here… except for the image. You’re going to have to insert one of these tags into the src attribute of the HTML <img> tag. That requires going into the raw code view, which you can access by clicking the <> button in the WYSIWYG editor’s toolbar.

A few key tags:

This is your code for the URL of the image. Yes, it has to be put into the src attribute of the <img> tag directly. There’s not a way that I could find to get MailChimp to recognize an <enclosure> as being an image and display it inline.

This will display the location name, if you added it to the <source> tag.

*|RSSITEM:DATE:F j - g:i a|*
I just though I’d point this out: you can customize the way MailChimp shows an event date by inserting a colon and a standard PHP date format into the *|RSSITEM:DATE|* tag. Nice!

If you’re interested in a nice layout with the featured image left aligned and the event info next to it, here’s something you can work with. Paste this in its entirety into the WYSIWYG editor’s raw code view in place of whatever you have in there now. Yes, inline CSS… welcome to HTML email!

<div style="clear: both; padding-bottom: 1em;">
<img src="*|RSSITEM:ENCLOSURE_URL|*" style="display: block; float: left; padding-right: 1em; width: 100px; height: 100px;" />
<h2 class="mc-toc-title" style="text-align: left;"><a href="*|RSSITEM:URL|*" target="_blank">*|RSSITEM:TITLE|*</a></h2>

<div style="text-align: left;"><strong>*|RSSITEM:DATE:F j – g:i a|*</strong><br />

<div style="clear: both; content: ''; display: table;">&nbsp;</div>

Update! I have encapsulated this functionality, along with some configuration options, into a plugin. You can download it from the WordPress Plugin Directory.

Find the mode of an array in PHP

For those of you who don’t remember studying statistics in math (and I barely do), the mode refers to the value that occurs most frequently in a set of data. That contrasts with the mean — what most of us call the “average” — and the median, which is the “middle” value if you sort all of the values in order.

My daughter was recently studying all of this and it brought it back to my mind. These are really not things I use often. But, as it happens, right now in my work I have a need for a PHP function that determines the mode in a set of data.

In this case, it’s not actually numbers; it’s dates. In short, I have an array of dates, and I need to know which date occurs the most often in the array. You’d think there would be a built-in PHP function for this, likely called array_mode() or else something long and completely illogical, or short and incomprehensible. But alas, there is no array_mode() function.

Fortunately, it’s pretty damn easy to write one. I found some examples on other sites, but they weren’t pithy enough for my tastes, so I rolled my own. Now you don’t have to:

function array_mode($arr) {
  $count = array();
  foreach ((array)$arr as $val) {
    if (!isset($count[$val])) { $count[$val] = 0; }
  return key($count);

Perhaps it’s excessive even to bother casting $arr as an array, but it’s a habit I picked up a long time ago and can’t seem to shake. Anyway, there you have it. (Of course, this probably breaks if $val isn’t scalar, but I’ll leave that to you to fix.)

Shut off and lock down comments on your WordPress site with 5 lines of SQL

Comments are kind of passé. Well, OK, they’re still everywhere, but they’re almost universally garbage. Meaningful discussion happens on social media these days, even if it’s prompted by a blog post. And if you’re using WordPress as a general-purpose CMS rather than just as a blogging tool, then you probably have no use for comments whatsoever.

Yet, they’re built in, and they’re a spam magnet. Even if your templates aren’t actually showing comments anywhere, the default WordPress settings allow comments to come in, cluttering up your database and nagging you with a disconcertingly large number in a bright red circle in the WordPress admin bar.


Fortunately, if you have direct database access and the fortitude to run a few simple lines of SQL, you can quickly accomplish the following:

  1. Purge all queued spam and pending comments (while safely retaining any old, approved comments for archival purposes
  2. Prevent any new comments from appearing on any of your existing posts/pages
  3. Prevent comments from ever being accepted on future posts/pages

The last of those is a simple setting. In WP admin, you can go to Settings > Discussion and uncheck the second and third boxes under Default article settings at the top of the page. Actually, uncheck all three of those. If you’re going to turn off incoming pings, you should turn off pingbacks. But my SQL code below doesn’t.


If you’re just starting a brand new WordPress site and you don’t ever intend to allow comments, just go and uncheck those boxes and you’re done. But if you’re trying to rescue a long-suffering WordPress site from drowning in spam, read on.

Here then in all of its glory is the magic SQL you’ve been looking for:

DELETE FROM `wp_comments` WHERE `comment_approved` != 1;
DELETE FROM `wp_commentmeta` WHERE `comment_id` NOT IN (SELECT `comment_ID` FROM `wp_comments`);
UPDATE `wp_posts` SET `comment_status` = 'closed', `ping_status` = 'closed';
UPDATE `wp_options` SET `option_value` = 'closed' WHERE option_name = 'default_comment_status';
UPDATE `wp_options` SET `option_value` = 'closed' WHERE option_name = 'default_ping_status';

Want to dissect what each of these lines is doing? Sure…

Line 1

DELETE FROM `wp_comments` WHERE `comment_approved` != 1;

This is going to delete all “pending” and “spam” comments. It leaves approved comments untouched. Note: you may have spam comments that are approved; one site I was just working on had thousands that were “approved” because the settings were a little too generous. I can’t give a catch-all SQL statement to address that problem, unfortunately. It requires analyzing the content of the comments to some extent.

You’d think maybe `comment_approved` = 0 would be better, but I found as I poked around that the possible values aren’t just 0 or 1. It may also be spam. It may be something else. (I haven’t researched all of the possibilities.)

Line 2

DELETE FROM `wp_commentmeta` WHERE `comment_id` NOT IN (SELECT `comment_ID` FROM `wp_comments`);

There’s a separate table that stores miscellaneous meta data about comments. There’s a good chance there’s nothing in here, but you may as well delete any meta data corresponding to the comments you just deleted, so here you go.

Line 3

UPDATE `wp_posts` SET `comment_status` = 'closed', `ping_status` = 'closed';

This is going through all of the existing posts — which don’t include just “posts”… “pages” are posts, “attachments” are posts… anything in WordPress is a post, really — and setting them to no longer accept comments or pingbacks. This doesn’t delete any comments on the posts that were already approved; it just prevents any new ones.


It’s the equivalent of going into every single post and unchecking the two boxes in the screenshot above. But it only takes a couple of seconds. FEEL THE AWESOME POWER OF SQL!!!

Line 4

UPDATE `wp_options` SET `option_value` = 'closed' WHERE option_name = 'default_comment_status';

Remember that screenshot near the beginning of this post, showing the three checkboxes under Settings > Discussion? Well this is the equivalent of unchecking the third one.

Line 5

UPDATE `wp_options` SET `option_value` = 'closed' WHERE option_name = 'default_ping_status';

And this is the equivalent of unchecking the second one.

So there you have it. No more comments, no more spam, no need for an Akismet account.

Postscript to my first experience running WordPress on PHP 7.0

This is a companion piece to my post from earlier today, When switching servers breaks code: a WordPress mystery.

It’s not much of a mystery anymore, but since this was my first, and somewhat unplanned, foray into working with PHP 7.0, I felt it was worthwhile to spend a bit more time today finally exploring which features from earlier versions are gone in PHP 7.0, so I’ll know if I am going to encounter any more challenges in the eventual, inevitable move to 7.0.

The best source I can find on the removals is this RFC from two years ago (!) that lists deprecated features under consideration for removal, all of which did eventually end up being removed. (I assume this is a complete list of the removals, since I can find no other more current list. But suffice to say it is clear that everything listed here was removed, even if it’s not all that was removed.)

I’m happy to see that the only thing in here that I have been using on any recent projects that was removed was the /e modifier for preg_replace(), which is what I uncovered as the source of the “mystery” in my last post. Of course, before this morning I’m not sure I even would have remembered that I was using it.

That said, I’m relatively confident that I am not using any of the other features, because while I had forgotten about the /e modifier, I at least know I use preg_replace() on a regular basis. Most of the other features in the list are things I don’t recall ever having seen before. Such is the nature of being a self-taught user of a language with over 5,000 built-in functions.

There’s only one other removed feature that I know I have used. A lot. It’s the mysql extension. It has been deprecated in favor of the mysqli extension for several years. But my own CMS, cms34, built originally in 2008 on the then-current version 1.2 of CakePHP, has mysql functions everywhere. I did eventually manage to update the platform to version 1.3.21 of CakePHP, but the leap to 2.0 was too arduous, and reworking hundreds of files to use mysqli functions was, likewise, something that never seemed justified. I stopped new development on cms34 in early 2014, but we still have many client sites running on it. I have committed to supporting them as long as necessary, but I am actively encouraging those clients to make the switch to WordPress.

The absolute incompatibility of cms34 with PHP 7.0 makes the merits of that switch a lot more obvious. And now the clock is ticking.

Bottom line for anyone who, like me, is still supporting legacy PHP 5 (or earlier!) code: change is coming.

Update! Well, OK, I was just searching in the wrong way. The official documentation for migrating from PHP 5.6 to 7.0 has lists of both removed extensions and SAPIs and changed functions.

When switching servers breaks code: a WordPress mystery

Earlier this week I launched a brand new WordPress site for a long-time client. Break out the champagne! But of course it’s never that simple, is it?

The client’s live server is a newly configured VPS running Ubuntu 16.04 LTS and PHP 7.0; meanwhile, our staging server is still chugging away on Ubuntu 14.04 LTS and PHP 5.5. So, clearly, a difference there. But I was pleased to find that, for the most part, the site functions perfectly on the new server.

But then the client discovered a problem: on one page, content from a custom post type query wasn’t displaying.

Here’s a short version of the pertinent code:

$people = new WP_Query(array(
  ‘order’ => ‘ASC’,
  ‘orderby’ => ‘menu_order’,
  ‘posts_per_page’ => -1,
  ‘post_type’ => ‘person’,

if ($people->have_posts()) {
  while ($people->have_posts()) {

      <header><h2><?php the_title(); ?></h2></header>
      <div><?php the_content(); ?></div>


Strangely, the_title() was working fine, but the_content() wasn’t. It had been — still is, in fact — working on our staging server, all other things within the WordPress context being equal. (Identical, up-to-date versions of the theme files and all plugins, and WP itself.) And the client confirmed that the content was present in WP admin.

I found, confusingly, that get_the_content() works, even though the_content() doesn’t. But of course you don’t get all of the proper formatting (like paragraph breaks) without some WP filters that the_content() applies, so I tried this:

<?php echo apply_filters(‘the_content’, get_the_content()); ?>

Still didn’t work. After a bit more research I was reminded that the pertinent function that filter runs is wpautop(), so I just called that directly:

<?php echo wpautop(get_the_content()); ?>

Now I have the content displaying nicely, but this is clumsy and I really do not get what might be different. I know the new server is running PHP 7.0 and our staging server is running PHP 5.5… but I’m struggling to understand what kind of changes in PHP could cause this specific problem.

Since get_the_content() works, and the_content() doesn’t, the problem has to lie in something that’s happening with the filters on the_content(). Why? Because the_content() calls get_the_content() right up front. In fact, there’s not a lot to the_content() at all. This function lives in wp-includes/post-template.php (beginning at line 230 in WP 4.6). Here it is in its entirety (reformatted slightly for presentation here):

function the_content( $more_link_text = null, $strip_teaser = false) {
  $content = get_the_content( $more_link_text, $strip_teaser );

  * Filters the post content.
  * @since 0.71
  * @param string $content Content of the current post.
  $content = apply_filters( ‘the_content’, $content );
  $content = str_replace( ‘]]>’, ‘]]>’, $content );
  echo $content;

As you can see, it’s really just 4 lines of actual code. It calls get_the_content() to retrieve the content, applies filters, does an obscure string replacement (which I think I understand but is not really pertinent here), and then echoes the results out to the page.

It’s pretty clear to me that the problem has to lie in one (or more) of the filters in the 'the_content' stack. I have to admit that even after years of working with it, I only have a rather nebulous understanding of how hooks work, so I’m not even sure where to begin dissecting the filter stack here to pinpoint the source of the trouble.

Whenever I know something works in one place and doesn’t work in another place, the first course of action in troubleshooting is to try to identify all of the differences between the two environments. Obviously we have some big differences here as I noted at the top of this post. But I am going to assume that the problem does not lie at the OS layer. Most likely it’s either a difference between PHP 5.5 and 7.0, or, even more likely, a difference between the PHP configurations on the two servers… specifically, modules that are or are not active. See my previous post on The Hierarchy of Coding Errors for my rationale here. Also keep in mind that I personally was responsible for installing LAMP on the server and configuring PHP, and it’s pretty obvious that we’re looking at the sysadmin equivalent of #1 or #2 in that list.

The next step, were I to care to pursue it much further (and if I didn’t have 200 other more important things to do, now that I have the problem “fixed”), would be to run phpinfo() on both servers and identify all of the differences.

That’s one possible path, at least. Another thing to consider is that the_content() actually is working just fine in other parts of the site, so maybe it would be worth digging into that WordPress filter stack first.

At this point, because as I said I have a few other more important things to work on, I will probably leave the mystery unresolved here. But I’d welcome any ideas from readers as to an explanation for all of this.

Update! I just couldn’t leave well enough alone, so a few minutes after I published this post, with the client’s permission, I restored the old version of the template, turned on WP_DEBUG and installed the Debug Bar plugin. Jackpot! Debug Bar returned the following error message when I was calling the_content(), but not when I had my “fixed” code in place:

Screen Shot 2016-09-01 at 9.24.16 AM

Well, how about that? As it turns out, the problem is due to a filter I myself had added, using a previously written function. (That’s #3 on the hierarchy list.) Combine that with deprecated functionality that was removed in PHP 7.0, and problem solved. And I even figured out why the problem is only occurring on this page and not site-wide… because my filter only runs if there’s an email address link in the content.