How I Built It

How I built it: Plugin Notes Plus

I released Plugin Notes Plus on the plugin repo in February, and it recently reached 200+ active installations. Although I built the plugin to “scratch my own itch,” it’s gratifying to know that others are finding it useful as well. I’m a big fan of the “How I Built It” podcast, so I thought it would be fun to write a post explaining how I built Plugin Notes Plus.

The Problem

The team I work with has notes in Evernote and Google Sheets with information about the plugins installed on the various sites we maintain. Crucially, some of those notes contain important information that one should know when updating the plugin. But those notes can be hard to find. What would be ideal would be to have a note right on the WordPress Admin Plugins page that would contain or link to important information about that plugin.

A survey of the existing offerings

I wasn’t the first one to have the idea to make a plugin notes plugin. Chris Coyier proposed the idea in 2009, and it was implemented by Mohammad Jangda with his plugin called Plugin Notes. I tested the plugin, and it worked as advertised except for one crucial flaw. When you click ‘delete’ to remove a note, it triggers the process of deleting the entire plugin. I found this support thread where the issue was identified, but the plugin doesn’t appear to be actively maintained.

Listening to an interview with Russell Aaron on How I Built It, I learned that the Maintainn Tools plugin offers a similar plugin notes functionality, among other things. While I liked how Maintainn Tools keeps plugin notes in a separate column rather than below the plugin description like Plugin Notes, I found it limiting that I couldn’t edit or delete the plugin notes that I had created.

Considering the limitations of the available offerings for plugin notes, I decided that there was an opportunity to build something that would better serve my team’s needs, and hopefully be of use to others as well.

How I built it

The basic idea behind the plugin is simple. When a user goes to their WordPress admin dashboard and views their installed plugins, Plugin Notes Plus provides an additional column to the right of the plugin descriptions for plugin notes. The user can add, edit, or delete notes for any installed plugin.

The plugin adds a column on the admin plugins page called “Plugin Notes” and provides functionality to add, edit, or delete notes about a plugin.

In the following sections, I describe the various considerations behind how I built the plugin in more detail.

The foundation: WordPress Plugin Boilerplate

Object-oriented programming (OOP) is a good approach for building plugins since it allows you to keep your code well organized and minimizes the chances that the code will conflict with other plugins or themes. I had learned the concepts behind OOP in introductory programming classes years ago, but I found Know the Code tutorials very helpful in getting me started with OOP PHP.

I decided to use the WordPress Plugin Boilerplate as the foundation for my plugin because it was well organized and helped to ensure that I was following best practices for plugin development. The boilerplate is organized into three main folders: ‘admin’ (for functionality in the admin), ‘public’ (for functionality on the front end), and ‘includes’ (for functionality that is shared between between the admin and front end). Because my plugin only deals with the WordPress admin, I removed the ‘public’ folder as well as any references to its files.


The user interface

Although the plugin seems simple, I spent a fair amount of time thinking through exactly how I wanted it to work. I liked how Maintainn Tools put the plugin notes in their own column and allowed for multiple notes per plugin, but I also wanted to make it possible to edit and delete notes. Thus, I used jQuery to build an interface that enables users to easily add, edit, and delete notes, and I used Ajax to handle database updates so that users can manipulate their notes without requiring a page refresh.

Note icons

I had the idea of including an icon next to each note to quickly convey what type of content it contains (e.g., info, warning, link, etc.). For that, I used Dashicons, which is the default icon font of the WordPress admin. I selected six icons that are available by default, but I also included a filter to enable developers to modify which icons are available.

Hyperlinks in notes

A feature that I liked in the original Plugin Notes plugin was that it automatically converted links to target="_blank". Presumably, if someone is looking at the Plugins page and clicks on a link in a note, the don’t want to actually leave the Plugins page.

Additionally, I converted any urls without <a> tags into links. Below is a snippet that uses a regular expression to identify any urls that aren’t links and converts them to links. I then used jQuery to add target="_blank" to all links.

function convert_urls_to_links( $input ) {
    $url_without_tags_regex = "/<a.*?<\/a>(*SKIP)(*F)|https?:\/\/\S*[^\s`!()\[\]{};:'\".,<>?«»“”‘’]/";
    $replacement_pattern = '<a href="$0">$0</a>';
    return preg_replace( $url_without_tags_regex, $replacement_pattern, $input );

Data storage

We all make mistakes, right? Well, I made a mistake when I decided where to store the plugin notes. I was familiar with the options API, so I decided to use it to store plugin notes in the options table. For each plugin with notes, I set up an array containing each plugin’s notes and note metadata and used add_option(), delete_option(), update_option(), and get_option() to handle adding, deleting, editing, and retrieving plugin notes, respectively.

The problem is that the options table is only meant to hold small amounts of information that don’t get updated often – typically setup and configuration information. Storing large amounts of data in the options table can impact performance. In fact, WP Engine will warn customers and even delete entries in the options table if they are too large.

Granted, I wouldn’t expect anyone to use Plugin Notes Plus to write an epic novel 😉 , but it’s still not a best practice to store plugin note data in the options table, as I learned after releasing the plugin and getting feedback from some early users. Returning to the codex, I reviewed my options for storing plugin notes in the database and decided that it would make the most sense to store plugin notes in their own custom database table.

In my first update (v1.1.0), I set up a new custom database table for plugin notes and added a migration routine to move any existing plugin notes out of the options table and into the new table. It felt a little like doing maintenance on a plane while flying it because I had to ensure that the update didn’t affect anyone’s existing plugin notes. However, the migration was successful, and I’m happy with how the plugin stores its data now.


Because the plugin collects and stores data in the database, security considerations were a must. Below is an overview of the security best practices that I followed while building the plugin. (I used the WordPress Plugin Handbook as a reference.)

Checking user capabilities

Any plugin that allows users to submit data should check that the user has the correct level of permissions. In the case of Plugin Notes Plus, I checked to ensure that the user had the activate-plugins capability before rendering the plugin notes and form.


After checking user capabilities, the next level of security is ensuring that the user actually intends to perform a given action. Nonces are unique, generated numbers that verify the origin and intent of requests.

Because I was using Ajax to update plugin notes, I generated a nonce using wp_create_nonce() and then made it available in the JavaScript file that handles plugin note updates using wp_localize_script(). I then checked that the nonce from the Ajax request matched the original nonce using check_ajax_referer().

Data validation and sanitization

Whenever notes are saved to the database or retrieved for rendering, they need to be validated and sanitized. For that, I wrote a process_plugin_note() function that uses wp_kses(), which filters a string of HTML to ensure that it includes only an allowed set HTML tags and attributes. For rendering translatable strings, I relied on the esc_html__() and esc_html_e() functions.

Safe database operations

Having set up a custom database table for plugin notes in the v1.1.0 release, I needed to ensure that I was following best practices when dealing with the table. I took advantage of WordPress’s built-in helper methods, including insert, update, and get_row. I also used the prepare method where appropriate to ensure that I generated SQL that was safe from SQL injection.

As an example, to retrieve a note with a particular ID, I used $wpdb->prepare() as follows:

 * Get a specific plugin note by id.
 * @since    1.1.0
public function get_plugin_note_by_id( $note_id ) {

    global $wpdb;
    $table_name = $wpdb->prefix . Plugin_Notes_Plus::get_table_name();

    $result = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM $table_name WHERE ID = %d;",
    ) );

    $note_array = array();
    $note_array['note'] = $this->process_plugin_note( $result->note_content );
    $note_array['icon'] = $result->note_icon;
    $note_array['user'] = $result->user_name;
    $note_array['time'] = $result->time;

    return $note_array;


There were a number of ways that I could have designed the plugin to work on multisite installations. One approach would be to have the plugin notes synced across all sites. However, I opted to have each site maintain its own plugin notes. I figured that the super admin might want to keep their own private plugin notes, and then each site could add their own notes as it made sense.

One interesting issue that I encountered was that, in a multisite install, the plugin notes column didn’t display for the super admin. I eventually discovered that the manage_plugins_columns and manage_plugins_custom_columns hooks that I was using to create the new column didn’t work for the super admin. To display the plugin notes column on the network admin (super admin) plugins page, I had to additionally use these hooks: manage_plugins-network_columns and manage_plugins-network_custom_column.


I set the language for one of the sites in my local multisite install to Spanish so that I could verify that all user-facing strings could be translated. Following the instructions in this article, I used Poedit to create a Spanish translation of my plugin. I was able to confirm that all of the translated strings displayed correctly in my Spanish install.

In conclusion…

I really enjoyed building Plugin Notes Plus, and I learned a lot about plugin development. Feel free to check out the project on GitHub, or you can find it in the plugin repo if you’d like to write some plugin notes of your own.

How I Built It

How I built it: A WordPress plugin to calculate user meeting times that uses the WP REST API and React.js

This post is based on a talk that I gave at the East Bay WordPress Meetup on December 10, 2017. The complete code for the plugin can be found on GitHub.

The problem

I work with a team of six that is distributed across the globe. In order to work effectively as a team, we hold regular meetings over video. Shortly after I joined the team, I noticed that there was sometimes confusion over when the meetings would begin. With team members in six different timezones, the confusion was understandable.

Our team lives in six different timezones across the globe. (Image adapted from Free Vectors by

The initial solution

Initially, I wrote a simple Python app on Google App Engine, using the pytz library to calculate user meeting times. The app listed each team member and their timezone. To calculate meeting times, a user would enter the meeting time and date and select a reference timezone. The app would then generate a list of team members and their personalized meeting times, which we could paste into a task in our project management system.

The Google App Engine solution served its purpose, basically eliminating any confusion about when our team meetings would start. However, it was somewhat hard to find and hard to use. For example, if a team member moved to a new timezone, they would have to look up the correct name of their new timezone in this database. If they didn't enter it correctly, it would throw an error. I could have spent time improving the interface, but I was focusing on improving my skills as a WordPress developer, and it worked well enough.

An opportunity to play with shiny, new things

With the introduction of the WP REST API and WordPress's adoption of React.js as the JavaScript framework behind the Gutenberg editor, I was looking for an excuse to build something with these new tools. A meeting timezone calculator seemed like a good first project. Additionally, building it as a WordPress plugin meant that it would be easier for our team to find – because it could be installed on our professional development blog, a site that we are already logged into on a daily basis.

How I built it: The WordPress part

The WordPress part of the plugin is quite simple. First, it creates a custom user meta field (called user_timezone) that can be set via a User Timezone dropdown field when editing a user's profile in the WordPress admin. The timezone options are borrowed from the "Timezone" dropdown menu that appears under Settings > General in a regular WordPress install.

The plugin creates a custom user meta field for the user's timezone.

In order for user timezones to be accessible via the REST API, I had to add them to the users WP API endpoint. During my testing, I discovered that only users who have published content are included in the users WP API endpoint by default. Since I wanted all registered users to be available to my plugin, regardless of whether they had published content, I first added a filter to remove 'has_published_posts' from the WP API user query.

// Includes all users in API result even if they haven't published posts
add_filter( 'rest_user_query', 'red_remove_has_published_posts_from_wp_api_user_query', 10, 2 );
 * Removes `has_published_posts` from the query args so even users who have not
 * published content are returned by the request.
 * @see
 * @param array           $prepared_args Array of arguments for WP_User_Query.
 * @param WP_REST_Request $request       The current request.
 * @return array
function red_remove_has_published_posts_from_wp_api_user_query( $prepared_args, $request ) {
    unset( $prepared_args['has_published_posts'] );
    return $prepared_args;

Then I made the user_timezone user meta value available to rest API by modifying the default response. I found it helpful to use Postman to confirm that I had successfully modified the default WP API users endpoint. The URL where you would find data for the users endpoint is:

// Make user_timezone user meta value available to rest API
add_action( 'rest_api_init', 'create_api_user_meta_field' );
function create_api_user_meta_field() {
    register_rest_field( 'user', 'user_timezone', array(
            'get_callback'    => 'get_user_meta_for_api',
            'schema'          => null,
function get_user_meta_for_api( $object ) {
    //get the id of the user object array
    $user_id = $object['id'];
    //return the user meta
    return get_user_meta( $user_id, 'user_timezone', 1 );
Users API output for Jamie, including user_timezone. Viewed using Postman.

Finally, I created a page under Admin > Tools called "Meeting Timezones" to display the interface where users can calculate meeting times. The PHP part of the plugin simply inserts an empty div with id="mtg-tz-container and then enqueues the React-generated styles and scripts.

A new page created under Tools called "Meeting Timezones."

How I built it: The React part

The React part is where the magic happens, transforming the empty div shown above into this:

The user interface for the meeting timezone calculator, generated by React.

The beauty of a front-end JavaScript library, like React.js, is that it enables you to have an app that responds to user input without having to reload the page. That's a huge advantage for a tool that runs in the WordPress admin dashboard, where page loads are notoriously slow.

React is a modular framework, and each component can handle two types of data: props and state. Props are values that don't change, while the state contains values that can be updated. In my meeting timezone calculator app, there is an outer component called <MtgTzContainer />, and it makes a call to the WP REST API to retrieve the user data, including user timezones. It stores that data in its state and then renders two components: <UserTimezoneList /> and <MeetingTimeForm />. It passes the user data to those components as props.

The <UserTimezoneList /> component renders the top section of the user interface that shows each user's name, avatar, and timezone. I also included a checkbox next to each user to indicate whether that individual would be included in the final meeting times list.

The <MeetingTimeForm /> component provides a datepicker and timezone dropdown menu. The datepicker is a component borrowed from an external library called react-datepicker, while the timezone dropdown menu is simply a select element whose options are a list of the unique timezones represented by the list of users. This component is where a lot of the work happens. It relies on the libraries moment.js and moment-timezone.js to calculate meeting times in various timezones. Finally, it passes the results to the <UserMeetingTimes /> component, which displays the output.

While building the React app and WordPress plugin, I ran into a few interesting issues:

  • The names of the .js files that React generates are modified with cache-breaking hashes. For example, main.js might become main.a8429063.js. Since WordPress requires you to enqueue scripts by name, and since that name will change every time the file is generated, I used the strategy explained here to look for and enqueue any file in the targeted folder that ends in .js.
  • I found that I needed to enqueue the script generated by React late so that the empty div that React looks for exists when the script is loaded.
  • While working on the React app locally (and before integrating it into the WordPress plugin), I needed to provide it some test user data from a WordPress API. I accomplished that by using a hardcoded URL to my personal site, where I had set up some dummy user data. However, when the app is integrated into a WordPress plugin, I want it to look for the users WP API endpoint for the site on which it's installed. To account for both situations, my script first looks at the current URL to see whether it contains /wp-admin. If so, it loads data from the current site's WP API. Otherwise, it catches the error and loads data from the dummy API. 

And you can, too!

This was my first foray into React.js. I got started with some free tutorials on YouTube. Fortunately, there are so many learning resources for React these days that you can easily find one that fits your learning style. I was also lucky to have create-react-app at my disposal, which made it very easy to get up and running with a React app on my local machine.

Figuring out how to write a WordPress plugin that uses React was a bit of a challenge. However, I found this post that walks you through a very simple way to get a WordPress plugin up and running with React. Instead of using a package manager and doing things the "right" way, it shows you how to load React and its dependencies from external URLs.

The author of that post acknowledges that it's not the "correct" way to do it. However, that simple approach helped me get started and, from there, I was able to figure out how to build my React app in the proper way, using node package manager and generating the final script that I can then package with the plugin. A future improvement would be to write a script that packages the React-generated scripts with the final WordPress plugin automatically. Additionally, I'd like to learn more about Redux, which is a tool for organizing state within an app.

The completed project can be viewed on GitHub.