Part 1 – Initial idea, design, and first-pass implementation
Part 2 – Building the product box that gets displayed in posts
Part 3 – Refactoring the codebase into classes, views, and separate files
Part 4 – Adding image uploading, shared plugin options, and uninstallation
Day 9: Image upload and display
Next up: adding the Image Upload / Select feature to the plugin. When the user creates a new Product Box, they should be able to pick an image for it (not just paste in a URL, that’s too janky for this fancy plugin). Furthermore, the user also needs to be able to edit that image choice when editing an existing Product Box.
WordPress already has a media management page/popup.
That’s this thing:

I just needed to hook into this feature from my plugin’s page. I used this tutorial as a starting point.
I didn’t use quite the same structure as this tutorial recommends. I wanted all of my form HTML in the same view files, not echoed in by a php function elsewhere in the codebase, but I did need to add an admin.js file and enqueue it as shown here, in my ‘init’ action. The tutorial left this step out so I’ve included it here:
add_action( 'init', function() {
include dirname( __FILE__ ) . '/includes/class-amazin-product-box-admin-menu.php';
include dirname( __FILE__ ) . '/includes/class-amazin-product-box-list-table.php';
include dirname( __FILE__ ) . '/includes/class-form-handler.php';
include dirname( __FILE__ ) . '/includes/amazin-product-box-functions.php';
// WordPress image upload library
wp_enqueue_media();
$jsurl = plugin_dir_url(__FILE__) . 'admin.js';
wp_enqueue_script('admin', $jsurl, array( 'jquery' ), 1.1, true);
...
The images are stored in my post content object by their ID (look at the very end – “215” is the image’s ID).
{"productName":"The Rainbow","productTagline":"It\'s got all the colors of the spectrum","productDescription":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce pulvinar, leo at cursus finibus, lectus massa tincidunt nulla, vitae lobortis lectus orci vitae nunc. Aenean a elit mollis, iaculis felis sed, consectetur velit. ","productUrl":"http://wow.com","productButtonText":"See the rainbow on Amazon.com","productImage":"215"}
I used wp_get_attachment_url() to turn the ID into the full path to the image. This is how it looks in the html that renders the actual product box in the post:
<img src="<?php echo wp_get_attachment_url( $content['productImage'] ) ?>"/>
Here’s my New Product Box page as the user first sees it:

And here’s my Edit Product Box page displaying an image the user already chose for a product box:

And here it is in a post!

This merged pull request shows the complete image uploading code (plus some bonus CSS styling and tagging to make it all look a bit better).
Day 10: Making the plugin’s settings page
I thought it’d be cool if all the product boxes shared a “headline” phrase, such as “We recommend” or “Our choice”. The user should edit in one place instead of on a per-box basis (though maybe a per-box override could be added, too, in the future).
WordPress calls this sort of thing an “option” and makes it very easy to set/get them. (This tutorial was helpful for identifying which hooks to use.)
Unlike everything else so far, the settings are not saved as posts. They’re saved as options in the _options table. Since all plugins (and WordPress itself) saves their plugins here, it’s important that the option have a unique name. Here’s my new option along with the string I gave it:

I wanted my plugin options to be adjustable from the plugin page (rather than a separate settings/options page elsewhere in the dashboard like some plugins do) so I simply added another form below the list table:

Here’s the form HTML (you can find this in view/product-box-list.php)
<form method="post" action="options.php">
<?php settings_fields( 'amazin_product_box_options_group' ); ?>
<h3>Product box settings</h3>
<p>These settings are shared by all product boxes on your site.</p>
<table>
<tr valign="top">
<th scope="row">
<label for="amazin_product_box_option_headline">Product Box Headline</label>
</th>
<td>
<input type="text" id="amazin_product_box_option_headline" name="amazin_product_box_option_headline" value="<?php echo get_option('amazin_product_box_option_headline'); ?>" />
<br/>
<span class="description"><?php _e('Examples: "We recommend", "Our pick", "A Sitename Favorite", etc.', 'apb' ); ?></span>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
I also had to add it as an option and register the settings in amazin-product-box-plugin.php:
add_action( 'init', function() {
//other init code here
add_option( 'amazin_product_box_option_headline', 'We recommend');
register_setting( 'amazin_product_box_options_group', 'amazin_product_box_option_headline', 'amazin_product_box_callback' );
And finally, I modified the HTML that displays the actual in-post product box to echo out the setting value instead of a hard-coded string (also in amazin-product-box-plugin.php):
...
<p class="amazin-product-box-recommend-text"><?php echo get_option('amazin_product_box_option_headline'); ?></p>
...
Here’s the product box displaying the text “Our choice” (it used to say “We recommend”) after I made the change in the plugin’s settings.

The complete code that adds plugin-specific options can be found in this merged pull request.
[Bug fixes and improvements] Adding a welcome banner, hiding search, completing the shortcode display, and fixing “sort by name”
Before moving onto a new feature I took a moment to make a few more improvements. There is now a “Welcome” banner at the top of the plugin page, the shortcodes now display in full for each table row, I hid the search bar, and I fixed sorting by name (it used to not work at all because it was trying to run the query on ‘name’ instead of ‘post_title’).
You can see all of that code here in this commit.

Day 11: Adding the uninstall script
First, I read the official guide on deactivating vs. uninstalling plugins. I figured if there was one place I really didn’t want to just fly blind, this was it.
My uninstall script needed to do the following:
- Delete the custom posts of type “amazin_product_box” from the _posts table
- Delete the plugin’s setting (“amazin_product_box_option_headline”) from _options
I went the route of making a standalone .php script. The example from WordPress’s developer guide shows dropping a table but that’s a bit extreme for my use, I just needed to delete the posts of type “amazin_product_box”.
Here’s what I did (it’s basically the example from WP’s own site with my own plugin’s names for things instead):
uninstall.php
<?php
// if uninstall.php is not called by WordPress, die
if (!defined('WP_UNINSTALL_PLUGIN')) {
die;
}
$option_name = 'amazin_product_box_option_headline';
delete_option($option_name);
// for site options in Multisite
delete_site_option($option_name);
// drop a custom database table
global $wpdb;
$wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE post_type='amazin_product_box'");
?>
With the uninstall script in place, I was able to deactivate and uninstall my plugin. I verified that the custom posts were gone from the _posts database and the headline option was gone from _options and from WordPress’s plugins directory on my server. Everything looked good.

The shortcode gets left behind in the posts, but I don’t think there’s anything I can do about that:

Finally, I reinstalled my plugin. None of the previously-created product boxes were present, which I expected since they were gone from the db, and I was able to create a brand new product box and stick it in a post.

Phew. I was dreading this part but the uninstall feature turned out to be the easiest, most “got it right on the first try” step of the whole project.
Here’s the commit that adds the uninstall script.
Day 12: Testing, bug fixes, and trying it out on a real website
The last thing I did was install the plugin on one of my actual sites and try it out as if I were an actual user.
I found a few bugs (including one that made the button not actually go anywhere, yikes) and had a few ideas for improving it, so I spent this last day on fixing those things.
- [Bug fix] – Editing a product box with an existing image now retains the existing saved image
- [Improvement] – Image upload help text added to Edit and New forms
- [Improvement] – Added a “plugin action link” that goes directly to Amazin’ Product Box management page via the plugin’s entry in the plugins page
- [Bug fix] – Button actually goes to the user’s link now
- [Improvement] – Added a setting for whether the button should open the link in a new tab or stay in the same tab
- [Improvement] – Product Box is now narrower than the post (I think it looks nicer that way)
You can see these fixes and improvements in this merge. I also did a small “security” pass in which I added code to prevent direct script access and removed an unused file. You can see that commit here.
Final thoughts
Here it is: my first WordPress plugin, just over 2 weeks after I started the project – looking exactly how I’d hoped.

I started blogging about product photography (and later smart home technology) ~5 years ago. I wanted to make my own custom plugins but I was just a baby programmer at the time and I got overwhelmed by terminology and just generally having no clue how to put something like it together.
Even now, after having worked professionally as a full-stack web dev for a few years and having completed most of a computer science degree, I was still a little intimidated by this project. Working in something new is always a bit uncomfortable at first.
Fortunately, this project wasn’t nearly as hard as I’d feared, and while I’m sure there’s something I got wrong with my first attempt at a plugin, I was only a little bit scared to deploy it on one of my live sites. :D
If you read this entire dev journal, thanks for following along – and if you see anything I could’ve done better, don’t hesitate to leave a comment or create a pull request on the project repo.
Resources
For future reference, here are some guides I found helpful for WP plugin development:
- WordPress.org’s Plugin Handbook
- What belongs in an uninstall script?
- WordPress Boilerplate Plugin Tutorial