Teardown Of A WordPress Multisite Events & Membership Site (Video/Slides/Code)

These are the slides from a WordPress meetup presentation that covered the code behind RSVPMaker, the RSVPMaker for Toastmasters extension, and a WordPress multisite instance where I offer free club websites as subdomains of toastmost.org.

The audience was at a mix of experience levels in terms of technical knowledge, with some people being more designers or business users of WordPress as opposed to programmers. I tried to frame it so those with more WordPress programming experience would have a chance of learning something new, and the others would at least get a glimpse of what is possible when WordPress is used as the basis for a community website.

To make this more useful, in addition to the slides and the video I’m sharing some of the relevant code snippets and pointers to some tutorials with more info on specific aspects.


How to create a plugin

Create a PHP file with a header like this and upload it into a subdirectory of wp-content/plugins

Plugin Name: Health Check
Plugin URI: https://wordpress.org/plugins/health-check/
Description: Checks the health of your WordPress install
Version: 0.1.0
Author: The Health Check Team
Author URI: http://health-check-team.example.com
Text Domain: health-check
Domain Path: /languages

Actually, for a one-off hack for your own site that you don’t intend to publish, it could be as simple as

Plugin Name: My Site Hack

Once this is uploaded, you should see the name of your plugin on the Plugins screen of the WordPress administration dashboard inviting you to activate it.

Making your plugin do something

The point of a plugin is to add to or modify the default behavior of WordPress. You do this by connecting with the plugin and filter hooks WordPress makes available. You can modify almost everything about how WordPress functions, which means you as a developer can do either wonderful or horrible things to a website. You can change the display of content, modify the database queries used to retrieve content, or alter security parameters for who should be able to access what.

The two major families of hooks are:

  • Actions triggered as WordPress loads a page like
    • ‘init’ – means the system has been initialized, but the process of running the page lookup query and outputting content has not yet begun)
    • ‘admin_init’ – same thing on the administration back end of WordPress
    • ‘wp_footer’ – fired by the wp_footer() function call that occurs in the footer of a properly coded theme. Often used to output a snippet of JavaScript or other code at the bottom of every public page or post.
  • Filters that allow you to modify some content or data
    • ‘the_content’ – change the content of a post
    • ‘the_title’ – change the title of the post
    • ‘query_where’ – change the WHERE clause in the SQL query used by the WordPress loop for retrieving a list of posts

Here’s an example of an action on ‘init’ used to set up the rsvpmaker post type. Without that, the system would not recognize urls pointing to rsvpmaker content.

add_action( 'init', 'rsvpmaker_create_post_type' );
function rsvpmaker_create_post_type() {
global $rsvp_options;
$menu_label = (isset($rsvp_options["menu_label"])) ? $rsvp_options["menu_label"] : __("RSVP Events",'rsvpmaker');
$supports = array('title','editor','author','excerpt','custom-fields','thumbnail');
  register_post_type( 'rsvpmaker',
      'labels' => array(
        'name' => $menu_label,
        'add_new_item' => __( 'Add New RSVP Event','rsvpmaker' ),
        'edit_item' => __( 'Edit RSVP Event','rsvpmaker' ),
        'new_item' => __( 'RSVP Events','rsvpmaker' ),
        'singular_name' => __( 'RSVP Event','rsvpmaker' )
    'menu_icon' => plugins_url('/calendar.png',__FILE__),
	'public' => true,
    'publicly_queryable' => true,
    'show_ui' => true, 
    'query_var' => true,
    'rewrite' => array( 'slug' => 'rsvpmaker','with_front' => FALSE), 
    'capability_type' => 'rsvpmaker',
    'map_meta_cap' => true,
    'has_archive' => true,
    'hierarchical' => false,
    'menu_position' => 5,
    'supports' => $supports,
	'taxonomies' => array('rsvpmaker-type','post_tag')
//more code for setting up taxonomy of event types

Here are a couple of filters.

function event_content($content) {
global $wpdb;
global $post;
global $rsvp_options;
if($post->post_type != 'rsvpmaker')
	return $content; // don't mess with other content
//add event dates up top
$content = rsvp_dates_block($post->ID) . $content;
		//process submission, show success / failure
$rsvp_on = get_post_meta($post->ID,'_rsvp_on',true);
		//display registration form
		$content .= show_rsvp_form($post->ID);
//return modified content
return $content;
add_filter('wp_title','date_title', 1, 3);
function date_title( $title, $sep = '»', $seplocation = 'left' ) {
global $post;
global $wpdb;
if($post->post_type == 'rsvpmaker')
	// get first date associated with event
	$sql = "SELECT meta_value FROM ".$wpdb->postmeta." WHERE meta_key='_rsvp_dates' AND post_id = $post->ID ORDER BY meta_value";
	$dt = $wpdb->get_var($sql);
	$title .= date('F jS',strtotime($dt) );
	if($seplocation == "right")
		$title .= " $sep ";
		$title = " $sep $title ";
return $title;

I’ve abbreviated the code of some of these plugins for simplicity, but the point is that the filter on the_content

  • Checks whether the content is associated with the post type rsvpmaker, and if not returns the content unchanged
  • Looks up the date or dates (stored as post metadata) and adds it to the top of the content.
  • Checks whether RSVPs (registrations) should be collected and if so displays the RSVP form at the bottom of the post.

The filter on wp_title changes what should be output in the HTML title tag in the page header. Again, it checks if the content is rsvpmaker content and if so adds the date.

Adding AJAX

In the Toastmasters application, when a meeting organizer is assigning other people to roles, the user ID of the chosen member is sent to the server via a JavaScript AJAX method so that it’s saved even if the meeting organizer doesn’t make it down to the bottom of the form and click Save.

This is accomplished with some JQuery Javascript

jQuery(document).ready(function($) {

$('.editor_assign').on('change', function(){
	var user_id = this.value;
	var id = this.id;
	var role = id.replace('editor_assign','');
	var security = $('#toastcode').val();
	$('#_project_'+role).html('Pick Manual for Project List');
	var post_id = $('#post_id').val();
	var editor_id = $('#editor_id').val();
	if(security && (post_id > 0))
		var data = {
			'action': 'editor_assign',
			'role': role,
			'user_id': user_id,
			'editor_id': editor_id,
			'security': security,
			'post_id': post_id
		jQuery.post(ajaxurl, data, function(response) {


and the PHP code that will accept this input and process it

add_action( 'wp_ajax_editor_assign', 'wp_ajax_editor_assign' );

function wp_ajax_editor_assign () {
global $wpdb;
$post_id = (int) $_POST["post_id"];
$user_id = (int) $_POST["user_id"];
$role = $_POST["role"];
$editor_id = (int) $_POST["editor_id"];
$timestamp = get_rsvp_date($post_id);
//my utility function to get member name based on ID
$name = get_member_name($user_id);
echo $name.' assigned to '.$role;
//tweak other settings, log change

The JavaScript detects when a select form field with the class .editor_assign has changed, gets the current value of that field and submits it to the server. The data is posted to a global variable ajaxurl (set automatically by WordPress) that points to a PHP file used specifically to process ajax requests. The submission must also include an ‘action’ attribute, in this case ‘editor_assign’ that WordPress will use to figure out what function to call.

The PHP code starts with an add_action call including that action string prefixed by wp_ajax_. This is the version that works for a logged in user — for an unauthenticated website visitor, it would be wp_ajax_nopriv_ or in this case wp_ajax_nopriv_editor_assign.

The add_action command identifies the function that will process the input, record it as post metadata, and send back a confirmation message to be displayed by the JavaScript routine.

Metadata for posts and users

My plugins make extensive use of custom metadata associated with posts and with users. The date of an event is stored as post metadata, and so is the 1 or 0 indicating whether the registration form should be displayed. The Toastmasters application modifies user profiles so they can contain data like home_phone and mobile_phone and Toastmasters ID # in addition to default fields like name and email address.

The pattern for getting and setting metadata is.

$rsvp_on = get_post_meta($post_id,'_rsvp_on',true);

//set this to 1

$home_phone = get_user_meta($user_id,'home_phone',true);

//update home phone

$log_messages_array = get_post_meta($post_id,'log_messages');

//add a log message rather than overwriting the previous value

In both cases, when getting metadata you must specify the third parameters as true if you are trying to retrieve a single value. By default, you will get back an array, which what be what you want if your application stores multiple items such as the log messages in this example under a single lookup key.

Here is what the corresponding database structure looks like for the wp_postmeta table, with metadata for an RSVPMaker post.

Post metadata

Modifying the Editor

When we create an RSVPMaker post, we need to add form fields to the standard WordPress editor and also functions for saving the data entered into that form as post metadata.

Here’s the WordPress editor, with the additional RSVPMaker fields showing under the editor.

RSVPMaker editor

Code to add the additional “metabox” on the form (simplified).

//admin options for an RSVPMaker post
add_action('admin_menu', 'my_events_menu');

function my_events_menu() {
add_meta_box( 'EventDatesBox', __('Event Options','rsvpmaker'), 'draw_eventdates', 'rsvpmaker', 'normal', 'high' );

function draw_eventdates() {
global $post;
global $wpdb;
global $rsvp_options;
global $custom_fields;
$custom_fields = get_rsvpmaker_custom($post->ID);
//display extra options on the WordPress editor form when viewing an RSVPMaker post
//get metadata for existing posts and display in the editor

The action ‘admin_menu’ triggers a function that registers our meta box, gives it a name, associates it with the function draw_eventdates, and says what kind of content it is associated with, in this case rsvpmaker posts.

The draw_eventdates pulls in all the metadata associated with the event so far and displays the form for setting event dates and other attributes.

Because there is a long list of options associated with setting up a registration form, those are hidden by default until the Collect RSVPs checkbox is checked. The code for displaying that section of the form looks like this.

jQuery(document).ready(function( $ ) {


$("#setrsvpon").change(function() {


When an RSVPMaker post is saved, the post title and content are saved just the way that they normally are. In the process, WordPress triggers the action post_save, which RSVPMaker uses to detect and save the additional data submitted from its additions to the editor form.


function save_calendar_data($postID) {

global $wpdb;

if($parent_id = wp_is_post_revision($postID))
	$postID = $parent_id;

// code to save dates as metadata goes here

	delete_post_meta($postID, '_rsvp_on', '1');

function save_rsvp_meta($postID)
$setrsvp = $_POST["setrsvp"];
foreach($setrsvp as $name => $value)
	$field = '_rsvp_'.$name;
	$single = true;
	update_post_meta($postID, $field, $value);

Shortcodes and Visual Representations of Shortcodes

WordPress shortcodes are placeholders for content to be inserted into the body of a post based on the output of a function.

Shortcodes are how this in the editor …

rsvpmaker_upcoming shortcode in the editor (Text view)

… turns into this on the website.

RSVPMaker event listing with calendar

A shortcode begins and ends with square brackets. The name of the shortcode is the first string of text after the opening bracket, and the shortcode can also include attributes like calendar="1" — which will be passed to the function to indicate the calendar should be displayed at the top of the event listings.

Here’s a simplified version of the rsvpmaker_upcoming shortcode

//function to render events listing
function rsvpmaker_upcoming($atts) {
// display events listing, using parameters from shortcode attributes
// uses a series of filters on sql query
add_filter('posts_join', 'rsvpmaker_join' );
add_filter('posts_groupby', 'rsvpmaker_groupby' );
add_filter('posts_distinct', 'rsvpmaker_distinct' );
add_filter('posts_fields', 'rsvpmaker_select' );
if(isset($atts["past"]) && $atts["past"])
	add_filter('posts_where', 'rsvpmaker_where_past' );
	add_filter('posts_orderby', 'rsvpmaker_orderby_past' );
	add_filter('posts_where', 'rsvpmaker_where' );
	add_filter('posts_orderby', 'rsvpmaker_orderby' );
//wordpress loop starts here

The excerpt shown here covers how RSVPMaker changes the standard WordPress query to look up a series of posts ordered by the date of the event rather than displaying them in blog post order of publication date. The $atts parameter is an array containing any shortcode attributes such as calendar="1" or past="1" (to display past instead of future dates).

If the shortcode uses a close tag like this

[my_shortcode]content here[/my_shortcode]

In that case, the shortcode function will also be passed a $content variable you can work with. Something like this:

function my_shortcode($atts,$content) {
   return $content;
   return '
'; }

To make shortcodes easier to use by non-techies, you can instead display an placeholder image in the body of the post, with a popup dialog box as a user interface for setting the shortcode attributes. You also want to add a new button in the visual editor for adding a new instance of your shortcode. What I present below is based on an excellent tutorial, Take your shortcodes to the ultimate level.

Popup editor for rsvpmaker_upcoming shortcode

Here’s an excerpt from the JavaScript code loaded as a plugin to the TinyMCE editor used by WordPress.

(function() {
	tinymce.PluginManager.add('rsvpmaker_upcoming', function( editor, url ) {
		var sh_tag = 'rsvpmaker_upcoming';

		//helper functions 
		function getAttr(s, n) {
			n = new RegExp(n + '=\"([^\"]+)\"', 'g').exec(s);
			return n ?  window.decodeURIComponent(n[1]) : '';

		function html( cls, data) {
		var urlparts = url.split("wp-content");
		var baseurl = urlparts[0] + '?rsvpmaker_placeholder=1';
		var placeholder = baseurl;
		var one = getAttr(data,'one');
		if(one && (one > 0))
			placeholder = placeholder.concat('&single_post='+one);
			placeholder = placeholder.concat('&calendar=' +  getAttr(data,'calendar') + '&events_per_page=' +  getAttr(data,'posts_per_page'));
			var type = getAttr(data,'type');
				placeholder = placeholder.concat('&event_type=' +  getAttr(data,'type'));
			data = window.encodeURIComponent( data );
			return '\n\n\n\n';
// full script includes code for extracting data from shortcode, displaying popup, rewriting shortcode with data from popup form

This script identifies shortcodes using the same regular expression (pattern matching) formula WordPress uses in PHP on the server side when rendering a post. It parses out the attributes and creates an HTML image tag that embeds those same parameters, so the data is logically associated with the image in the JavaScript object model. The src attribute of the img tag points to a url that has some of that data encoded in it, such as


Instead of referencing an image file, the img tag points to a url the plugin will use to generate an image.

//generate the image if called for
function rsvpmaker_placeholder_image () {
$impath = dirname( __FILE__ ).DIRECTORY_SEPARATOR.'placeholder.png';
$im = imagecreatefrompng($impath);
$im = imagecreate(800, 50);
imagefilledrectangle($im,5,5,790,45, imagecolorallocate($im, 50, 50, 255));
// White background and blue text
$bg = imagecolorallocate($im, 200, 200, 255);
$border = imagecolorallocate($im, 0, 0, 0);
$textcolor = imagecolorallocate($im, 255, 255, 255);
$text = __('Events','rsvpmaker').': ';
$tip = '('.__('double-click for popup editor','rsvpmaker').')';
//pull in data from query parameters
foreach ($_GET as $name => $value)
	if($name == 'rsvpmaker_placeholder')
	$text .= $name.'='.$value.' '; 
// Write the string at the top left
imagestring($im, 5, 10, 10, $text, $textcolor);
imagestring($im, 5, 10, 25, $tip, $textcolor);
// Output the image, then exit
header('Content-type: image/png');

Registered to execute on the admin action, before any content has been queried or output, this function first checks for the presence of a query string like ?rsvpmaker_placeholder=1 and returns if it is not found.

If that query string is present, the function loads a blank background image for the placeholder from the plugins directory, adds text based on the query parameters, and outputs the modified image. Finally, it exits before WordPress can output any other content.

Custom Administration Screens

Because RSVPMaker collects event registrations, we also need to add a screen on the administrative back end where those registrations can be viewed.

RSVP Report screen
add_action('admin_menu', 'my_rsvp_menu');
//register an additional option on the menu, as a submenu in the RSVP Events section.
function my_rsvp_menu() {
global $rsvp_options;
add_submenu_page('edit.php?post_type=rsvpmaker', __("RSVP Report",'rsvpmaker'), __("RSVP Report",'rsvpmaker'), 'edit_others_posts', "rsvp", "rsvp_report" );
//output the report or listing of events for which rsvps have been collected
function rsvp_report() {
global $wpdb; //grab the global object for the WordPress database
		$sql = $wpdb->prepare('SELECT * FROM '.$wpdb->prefix.'rsvpmaker WHERE id=%d ORDER BY timestamp DESC',$_GET["event_id"]);
		$results = $wpdb->get_results($sql);
			foreach($results as $row)
				// display rsvp results for each person registered such as $row->email
		// display listing of events to choose from

Custom Dashboard

The default WordPress dashboard shows updates on plugins and upcoming WordPress meetups. My Toastmasters community is more concerned about managing the agenda for upcoming events, so we present them with a custom dashboard.

Toastmasters dashboard

For a more detailed tutorial, see this from WP Explorer. Here’s a quick summary.

add_action('wp_dashboard_setup', 'awesome_add_dashboard_widgets',99 );
//register the dashboard widget
function awesome_add_dashboard_widgets() {
wp_add_dashboard_widget('awesome_dashboard_widget', 'WordPress for Toastmasters Dashboard', 'awesome_dashboard_widget_function');
global $wp_meta_boxes;
// manipulate the $wp_meta_boxes to put my widgets on top
//render the dashboard widget
function awesome_dashboard_widget_function() {
//generate Toastmasters-specific content for dashboard widget

Custom user registration and metadata

User records can also be customized to include metadata for your purposes. For Toastmasters club members, I want to record information such as phone numbers and Toastmasters ID #. Club officers are presented with an Add Member screen that asks for that additional information and records it as part of their user account, along with the required fields like user_login and user_pass (password).

//custom registration
$user["user_login"] = $_POST["user_login"];
$user["user_pass"]  = $_POST["user_pass"];
$user["user_email"]  = $_POST["user_email"];
$user["first_name"]  = $_POST["first_name"];
$user["last_name"]  = $_POST["last_name"];
$user["home_phone"] = $_POST["home_phone"];
//add existing user to blog within multisite install
add_user_to_blog($blog_id, $user_id,'subscriber');
//add metadata to user account
//get metadata variable
$tmid = get_post_meta($user_id,"toastmasters_id",true);

We can test for whether a user is logged in, a member of a specific blog (site within a multisite install), or possesses a specific capability under the WordPress security scheme.

	//do something for users only
	//do something for members only
	//show an administrator only option
	//only a user with the rights to edit events

Add Roles and Capabilities

WordPress security revolves around user roles and capabilities. Each of the predefined roles (subscriber, contributor, author, editor, administrator) has a set of default capabilities. That list of capabilities can also be manipulated by plugins. I like the plugin User Roles Editor as a general purpose utility or this purpose. I wrote my own routines to define plugin-specific capabilities and roles.

In this example, I add a Manager role (one level up from editor, not quite an administrator) and add a capability to the default administrator role.

//initialize the function
//add role if it has not previously been registered
function add_awesome_roles() {
$manager = get_role('manager');
add_role( 'manager', 'Manager', array( 'delete_others_pages' => true,
'delete_others_posts' => true,
'delete_pages' => true,
'delete_posts' => true,
'delete_private_pages' => true,
'delete_private_posts' => true,
'delete_published_pages' => true,
'delete_published_posts' => true,
'edit_others_pages' => true,
'edit_others_posts' => true,
'edit_pages' => true,
'edit_posts' => true,
'edit_private_pages' => true,
'edit_private_posts' => true,
'edit_published_pages' => true,
'edit_published_posts' => true,
'manage_categories' => true,
'manage_links' => true,
'moderate_comments' => true,
'publish_pages' => true,
'publish_posts' => true,
'read' => true,
'read_private_pages' => true,
'read_private_posts' => true,
'upload_files' => true,
'delete_others_rsvpmakers' => true,
'delete_rsvpmakers' => true,
'delete_private_rsvpmakers' => true,
'delete_published_rsvpmakers' => true,
'edit_others_rsvpmakers' => true,
'edit_rsvpmakers' => true,
'edit_private_rsvpmakers' => true,
'edit_published_rsvpmakers' => true,
'publish_rsvpmakers' => true,
'read_private_rsvpmakers' => true,
'promote_users' => true,
'remove_users' => true,
'delete_users' => true,
'list_users' => true,
'edit_users' => true,
"view_reports" => true,
"view_contact_info" => true,
"edit_signups" => true,
"edit_member_stats" => true,
"edit_own_stats" => true,
"agenda_setup" => true,
"email_list" => true,
"add_member" => true,
"edit_members" => true
 ) );
//add capability to an existing role
$administrator = get_role('administrator');

Making a WordPress Installation Multisite

See Create a Network in the WordPress Codex documentation for detailed instructions on configuring a single installation of the software to host a network of sites.

You start the process by modifying your wp-config.php file to include this statement:

define( 'WP_ALLOW_MULTISITE', true );

When you return to the admin screens, you essentially get a wizard that guides you through the process of determining what settings to change wp-config.php as well as your .htaccess file.

For the subdomain configuration I used, where sites have URLs like op.toastmost.org and demo.toastmost.org, I also had to change the DNS settings to recognize “wildcard” subdomains. Just as the path to a blog post represents a logical naming convention for content in the database, WordPress can then treat subdomains as aliases for sites within the database table.

When you make these changes, WordPress modifies the database for your site to include separate tables for each site. For example, instead of single wp_posts table, you get a whole series of posts tables named according to the subdomain site ID.

Multisite tables for posts

Similarly, your uploads directory gets divided up by site ID to keep those files separate.

Multisite directories.

Finally, I use a custom site registration process that gathers information specific to my application, such as the name of the Toastmasters club and the timezone (important for calendar functions) and adds default content and settings.

The process is:

  • Club website owner first must register for an account. Custom user account registration flow requires that they first register for a MailChimp mailing list. I use the MailChimp API to verify their email address is on the list before accepting their registration.
  • User fills out the form with the requested subdomain and other data.
  • The script below creates the site.
//data from registration form
$slug = strtolower(preg_replace('/[^A-Za-z0-9]/','',$_POST["slug"] ));
$domain = $_SERVER['SERVER_NAME'];
$title = $_POST['title'];
//slug (subdomain) + domain
$newsite = $slug.'.'.$domain;
//test for conflict with existing site
if(domain_exists($newsite, '/' ) )
  die($newsite ." already exists");
$blog_id = wpmu_create_blog($newsite, '/', $title, $site_owner);
	die("failed to create blog: " . $blog_id);


if(get_current_blog_id() == 1)
	die("Something went horribly wrong");


update_option('blogdescription','Where Leaders Are Made');
//get rid of default page
$pages = get_pages();
//create first post
$post = array(
	  'ID' => 1,
	  'post_content'   => "We're trying ".'<a href="https://wp4toastmasters.com">WordPress for Toastmasters</a>, a free service for Toastmasters clubs based on the <a href="http://wordpress.org">WordPress</a> blogging system.

<img class="alignnone size-medium wp-image-403" src="https://wp4toastmasters.com/wp-content/uploads/2014/11/wordpress-4-toastmasters-flat-300x300.png" alt="wordpress-4-toastmasters-flat" width="300" height="300" />',
	  'post_name'      => 'wp4toastmasters',
	  'post_title'     => "We're creating a WordPress for Toastmasters website",
	  'post_author'    => $site_owner,
	  'ping_status'    => 'closed'


	wp4toast_template($site_owner); // install standard meeting templates
	rsvptoast_pages ($site_owner);

	wp_update_term(1, 'category', array(
	  'name' => 'Club News',
	  'slug' => 'club-news'
	wp_create_category('Members Only');


We now have a custom-built website, with some starter content specific to its purpose as a Toastmasters website.

Questions? Drop me a line: david@rsvmpaker.com

The plugins discussed here are in the WordPress repository

You can also look them up (and contribute improvements!) on GitHub