Creating an add-on page with the REST API: Part 2

Example screenshot of an add-ons page shown as plugin cards in the WordPress admin

In part 1 of this tutorial series, I covered how to set up a custom REST API route for your Web site. Doing that allows others to get info they need from your site in a standardized JSON format and use it to “do stuff”.

Our route was for accessing plugins from our plugin portfolio. In particular, we created a method for accessing a specific plugin’s add-ons (or, child plugins).

In part 2, you’ll learn how to output a list of your plugin’s add-ons in the form of “plugin cards.”

I’m going to assume that you already know how to build an admin screen. If not, head over to the WordPress documentation for Administration Menus. This tutorial is only going to cover what you output to the screen. So, before moving forward, have your custom admin screen set up and ready to go.

Talking to the REST API

Our plugin, which is installed on a user’s site, needs a way to talk to the REST API on our site. Fortunately, WordPress already has us covered with its HTTP API. We’ll be making use of wp_remote_get() and wp_remote_retrieve_body() to handle the request.

In the following code, I’m using the term pluginslug. You should replace that with your plugin slug from part 1 in this tutorial.

For our plugin, we need to create a function for grabbing add-ons for the plugin and returning them for use on our admin screen.

function th_get_addons() {

    // Get the transient where the addons are stored on-site.
    $data = get_transient( 'pluginslug_addons' );

    // If we already have data, return it.
    if ( ! empty( $data ) )
        return $data;

    // Make sure this matches the exact URL from your site.
    $url = '';

    // Get data from the remote URL.
    $response = wp_remote_get( $url );

    if ( ! is_wp_error( $response ) ) {

        // Decode the data that we got.
        $data = json_decode( wp_remote_retrieve_body( $response ) );

        if ( ! empty( $data ) && is_array( $data ) ) {

            // Store the data for a week.
            set_transient( 'pluginslug_addons', $data, 7 * DAY_IN_SECONDS );

            return $data;

    return false;

You’ll notice that I’m setting a transient on the user’s site for 7 days. You can make that a shorter amount of time, but keep in mind that potentially 1,000s of sites could be trying to get data from your site. You’ll want to store that data for a while. Unless you’re releasing a new add-on for your plugin every few days, this data doesn’t need to be updated often anyway.

Displaying your add-on plugin cards

Unfortunately, like so many other UI components in WordPress, plugin cards are hardcoded into core with no way to reuse the component. So, we must roll our own functionality for this.

Creating the add-on card function

The first thing we need is a function for an add-on card. I’m reusing the core WP classes to avoid having to write extra CSS. Keep in mind that WP could change this on a whim. Feel free to roll your own classes and style this however you want.

Note that the fields we’re using are fields that we set up in part 1 of this tutorial series. If you changed them in your code, you’ll want to make sure they match here.

function th_addon_card( $addon ) { ?>

    <div class="plugin-card plugin-card-<?php echo esc_attr( $addon->slug ); ?>">

        <div class="plugin-card-top">

            <div class="name column-name">
                    <a href="<?php echo esc_url( $addon->url ); ?>">
                        <?php echo esc_html( $addon->title ); ?>

                        <?php if ( $addon->media->icon->url ) : ?>

                            <img class="plugin-icon" src="<?php echo esc_url( $addon->media->icon->url ); ?>" alt="" />

                        <?php endif; ?>

            <div class="action-links">

                <ul class="plugin-action-buttons">
                        <?php if ( $addon->meta->purchase_url ) : ?>

                            <a class="install-now button" href="<?php echo esc_url( $addon->meta->purchase_url ); ?>"><?php esc_html_e( 'Purchase', 'pluginslug' ); ?></a>

                        <?php elseif ( $addon->meta->download_url ) : ?>

                            <a class="install-now button" href="<?php echo esc_url( $addon->meta->download_url ); ?>"><?php esc_html_e( 'Download', 'pluginslug' ); ?></a>

                        <?php else : ?>

                            <a class="install-now button" href="<?php echo esc_url( $addon->url ); ?>"><?php esc_html_e( 'Download', 'pluginslug' ); ?></a>

                        <?php endif; ?>

            <div class="desc column-description">

                <?php echo wpautop( wp_strip_all_tags( $addon->excerpt ) ); ?>

                <p class="authors">
                    <?php $author = sprintf( '<a href="%s">%s</a>', esc_url( $addon->author->url ), esc_html( $addon->author->name ) ); ?>

                    <cite><?php printf( esc_html__( 'By %s', 'members' ), $author ); ?></cite>


        </div><!-- .plugin-card-top -->

        <?php if ( ( $addon->meta->rating && $addon->meta->rating_count ) || $addon->meta->install_count ) : ?>

            <div class="plugin-card-bottom">

                <?php if ( $addon->meta->rating && $addon->meta->rating_count ) : ?>

                    <div class="vers column-rating">
                        <?php wp_star_rating( array( 'type' => 'rating', 'rating' => floatval( $addon->meta->rating ), 'number' => absint( $addon->meta->rating_count ) ) ); ?>
                        <span class="num-ratings" aria-hidden="true">(<?php echo absint( $addon->meta->rating_count ); ?>)</span>

                <?php endif; ?>

                <?php if ( $addon->meta->install_count ) : ?>

                    <div class="column-downloaded">
                        <?php printf(
                            esc_html__( '%s+ Active Installs', 'pluginslug' ),
                            number_format_i18n( absint( $addon->meta->install_count ) )
                        ); ?>

                <?php endif; ?>

            </div><!-- .plugin-card-bottom -->

        <?php endif; ?>

    </div><!-- .plugin-card -->

<?php }

Outputting add-on cards on your admin screen

Now that you have a function for displaying an add-on card, you merely need to display them. The following code will handle that.

<?php $addons = th_get_addons(); ?>

<?php if ( $addons ) : ?>

    <?php foreach ( $addons as $addon ) : ?>

        <?php $this->addon_card( $addon ); ?>

    <?php endforeach; ?>

<?php else : ?>

    <div class="error notice">
        <p><strong><?php esc_html_e( 'There are currently no add-ons to show. Please try again later.', 'pluginslug' ); ?></strong></p>

<?php endif; ?>

And, that’s all there is to it. Have fun!

The numbers

From a business perspective, this addition to the Members plugin has consistently brought in an additional 1,300 visitors to Theme Hybrid each month. At the moment, there’s only 2 add-on plugins listed. The add-on screen is not something that would be visited often by a user.

Members is my most popular plugin with over 100,000 active installs. A bit more in-your-face admin advertising would probably bring in more visitors.

But, that’s not how I roll.

I like to integrate cleanly into the WordPress admin interface and not tick off my users. The extra visits every month are nice.


  1. I was thinking of doing this for a plugin that is in the WordPress repo. Do you think it would get flagged or remove because it goes against “phoning home”?

    1. In part 1 of this tutorial, I mentioned that I talked this over with the plugin review team. I was told it was OK.

      Technically, this isn’t “phoning home”. Phoning would be sending data from the user’s site back to my site. That’s not the case here. If it were, the user would have to explicitly opt into the feature.

      In this case, it’s the user’s site that is getting data from my site. To put it into perspective, another example of this sort of thing would be a Twitter widget listing all of a user’s tweets. They’d have to retrieve the tweets from, for example.

Comments are closed.