Create a custom HivePress extension
In this tutorial, we'll create a custom HivePress extension that allows users to:
  • Follow or unfollow any vendor
  • View listings from the followed vendors on a single page
  • Get emails about new listings from the followed vendors
  • Unfollow all vendors with one click
While this extension is pretty simple, it covers the main aspects of the HivePress framework, so by the end of this tutorial, you should be able to create your own extension for HivePress.
Before we begin, please make sure that you have a working WordPress installation and at least basic WordPress development skills. The result of this tutorial is available on GitHub, so you can browse the complete source code or download the extension to test it locally.

Create the main file

First, create a directory in the wp-content/plugins WordPress subdirectory. The directory name will be used as the extension identifier, so make sure it's unique enough to avoid conflicts with other HivePress extensions (use lowercase letters, numbers, and hyphens only).
For this tutorial, we'll name it "foo-followers", where "foo" is a unique prefix (e.g. your company name or abbreviation), and the "followers" part describes the extension purpose.
Next, create a PHP file with the same name inside the extension directory. This is the main file that is loaded by WordPress automatically when the extension is active.
<?php
/**
* Plugin Name: Followers for HivePress
* Description: Allow users to follow vendors.
* Version: 1.0.0
* Author: Foo
* Author URI: https://example.com/
* Text Domain: foo-followers
* Domain Path: /languages/
*/
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
// Register extension directory.
add_filter(
'hivepress/v1/extensions',
function( $extensions ) {
$extensions[] = __DIR__;
return $extensions;
}
);
As you can see, this file contains the extension details, such as:
  • Plugin Name & Description - the extension name and short description of its purpose;
  • Author & Author URI - your or your company name with an optional website URL;
  • Text Domain - used for translating the extension, matches the main file name.
If you plan to distribute your extension, please name it "Something for HivePress" rather than "HivePress Something" to avoid trademark violation.
Also, there’s a simple code that prevents direct file access by URL so that it can be loaded by WordPress only. We’ll add the same code to every file created in this tutorial:
defined( 'ABSPATH' ) || exit;
The only thing the main file does is register the extension directory via the hivepress/v1/extensions hook to allow HivePress to detect and load all the other extension files automatically:
add_filter(
'hivepress/v1/extensions',
function( $extensions ) {
$extensions[] = __DIR__;
return $extensions;
}
);
If you plan to submit your extension to the WordPress.org repository, make sure to follow its guidelines and add the readme.txt along with the license file.
After you create the main file, go to WordPress > Plugins and activate the extension:

Create a component

In HivePress, components are PHP classes used to group actions, filters, and helper functions. Let’s create an empty component and add the extension-specific functions to it later.
Create a class-followers.php file in the includes/components extension subdirectory. Notice that it has the class- prefix, and its name matches the extension name. It must be unique enough to avoid conflicts with other HivePress components (use lowercase letters, numbers, and hyphens only).
The PHP class name must be based on the file name, but with underscores instead of hyphens and no lowercase restriction (e.g. Foo_Bar class for the class-foo-bar.php file).
<?php
namespace HivePress\Components;
use HivePress\Helpers as hp;
use HivePress\Models;
use HivePress\Emails;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* Component class.
*/
final class Followers extends Component {
/**
* Class constructor.
*
* @param array $args Component arguments.
*/
public function __construct( $args = [] ) {
// Attach functions to hooks here (e.g. add_action, add_filter).
parent::__construct( $args );
}
// Implement the attached functions here.
}
If your extension is simple enough, you can fully implement it within a component and skip other steps, but we recommend using the HivePress framework where possible.

Create a model

In HivePress, models are PHP classes that represent the WordPress database entities such as posts, comments, or terms. Using models makes working with the database much easier.
Let’s create a Follow model for storing the follower ID along with the followed vendor ID. Create a class-follow.php file in the includes/models extension subdirectory. The file and PHP class naming conventions are the same as for components.
<?php
namespace HivePress\Models;
use HivePress\Helpers as hp;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* Model class.
*/
class Follow extends Comment {
/**
* Class constructor.
*
* @param array $args Model arguments.
*/
public function __construct( $args = [] ) {
$args = hp\merge_arrays(
[
'fields' => [
'user' => [
'type' => 'id',
'required' => true,
'_alias' => 'user_id',
'_model' => 'user',
],
'vendor' => [
'type' => 'id',
'required' => true,
'_alias' => 'comment_post_ID',
'_model' => 'vendor',
],
],
],
$args
);
parent::__construct( $args );
}
}
Notice that the model class is based on the Comment class. This means that the model objects will be stored as WordPress comments of a custom type. We implement the model this way because comments are linked to both users and posts in the WordPress database schema. Since vendors are implemented as posts of a custom type, we can use this model to store both the follower (user) and the followed vendor IDs.
The Follow model contains 2 fields:
  • user - mapped to the user_id database field, used for storing the follower ID;
  • vendor - mapped to the comment_post_ID database field, used for storing the followed vendor ID.
Also, if the model is based on the Comment class, WordPress will show the model objects in the comment feeds. To keep the model objects hidden, create a comment-types.php file in the includes/configs extension subdirectory:
<?php
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
return [
'follow' => [
'public' => false,
],
];
The code above contains a configuration that makes the follow comment type registered by the model we created private.

Create a template

In HivePress, templates are defined as PHP classes that contain arrays of blocks. With blocks, it’s easy to re-use and customize specific layout parts without affecting the whole template.
Let’s create a template for a page that displays all listings from the followed vendors. Create a class-listings-feed-page.php file in the includes/templates extension subdirectory. The file and PHP class naming conventions are the same as for components.
It’s good practice to follow the {entity}-{context}-{layout} pattern for naming templates (e.g. Listing_Edit_Page, Vendor_View_Block).
<?php
namespace HivePress\Templates;
use HivePress\Helpers as hp;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* Template class.
*/
class Listings_Feed_Page extends User_Account_Page {
/**
* Class constructor.
*
* @param array $args Template arguments.
*/
public function __construct( $args = [] ) {
$args = hp\merge_trees(
[
'blocks' => [
'page_content' => [
'blocks' => [
'listings' => [
'type' => 'listings',
'columns' => 2,
'_order' => 10,
],
'listing_pagination' => [
'type' => 'part',
'path' => 'page/pagination',
'_order' => 20,
],
],
],
],
],
$args
);
parent::__construct( $args );
}
}
As you can see, the template class is based on the User_Account_Page class. This means that the template inherits the user account page layout and adds custom blocks to it.
The template we created adds 2 blocks to the page_content area:
  • listings - displays listings for the current page;
  • listing_pagination - displays the page links for navigation.
The block names must be unique within the template scope. Each block is defined as an array containing the block type and extra parameters used to render the block.

Create a controller

Now, we need to define custom URLs that will render the template we created and perform specific actions, such as following or unfollowing a vendor. In HivePress, this can be done using controllers – PHP classes that define URL routes and implement actions corresponding to them.
Create a class-followers.php file in the includes/controllers extension subdirectory. The file and PHP class naming conventions are the same as for components.
<?php
namespace HivePress\Controllers;
use HivePress\Helpers as hp;
use HivePress\Models;
use HivePress\Blocks;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* Controller class.
*/
final class Followers extends Controller {
/**
* Class constructor.
*
* @param array $args Controller arguments.
*/
public function __construct( $args = [] ) {
$args = hp\merge_arrays(
[
'routes' => [
// Define custom URL routes here.
],
],
$args
);
parent::__construct( $args );
}
// Implement the route actions here.
}

Add the template route

Let's define a new URL route in the controller constructor:
'listings_feed_page' => [
'title' => esc_html__( 'Feed', 'foo-followers' ),
'base' => 'user_account_page',
'path' => '/feed',
'redirect' => [ $this, 'redirect_feed_page' ],
'action' => [ $this, 'render_feed_page' ],
'paginated' => true,
],
If you add new or change any of the existing URL routes, don't forget to refresh permalinks in the Settings > Permalinks section.
The route we defined uses these parameters:
  • title - used for the page title and menu label;
  • base - used for inheriting another route path;
  • path - the relative route URL path;
  • redirect - points to the URL redirect function;
  • action - points to the route action function;
  • paginated - flag required for paginated URLs.
Based on the parameter values, the listings_feed_page route has the /account/feed URL, renders a page with the “Feed” title and supports pagination.
Next, let's implement the redirect and action functions for this route below the constructor:
/**
* Redirects listing feed page.
*
* @return mixed
*/
public function redirect_feed_page() {
// Check authentication.
if ( ! is_user_logged_in() ) {
return hivepress()->router->get_return_url( 'user_login_page' );
}
// Check followed vendors.
if ( ! hivepress()->request->get_context( 'vendor_follow_ids' ) ) {
return hivepress()->router->get_url( 'user_account_page' );
}
return false;
}
/**
* Renders listing feed page.
*
* @return string
*/
public function render_feed_page() {
// Create listing query.
$query = Models\Listing::query()->filter(
[
'status' => 'publish',
'vendor__in' => hivepress()->request->get_context( 'vendor_follow_ids' ),
]
)->order( [ 'created_date' => 'desc' ] )
->limit( get_option( 'hp_listings_per_page' ) )
->paginate( hivepress()->request->get_page_number() );
// Set request context.
hivepress()->request->set_context(
'post_query',
$query->get_args()
);
// Render page template.
return ( new Blocks\Template(
[
'template' => 'listings_feed_page',
'context' => [
'listings' => [],
],
]
) )->render();
}
When the route URL is visited, the redirect function is called first. In our case, it checks if the current user is logged in and has any followed vendor IDs. It returns the corresponding redirect URL or false if all checks are passed.
If there was no redirect, the action function is called next. As you can see, it creates a query for listings published by the followed vendors, sets it as the main page query, and finally renders the template we created earlier. Notice that this function returns the rendered HTML instead of outputting it with echo.
Now, if you refresh permalinks in Settings > Permalinks and try to visit the /account/feed URL, you will be redirected because you haven’t followed any vendors yet.

Update the component

We already used a code that checks if the current user follows any vendors by checking the vendor_follow_ids value in the request context, but there’s no function that sets this value in context yet. Add this code to the component constructor:
add_filter( 'hivepress/v1/components/request/context', [ $this, 'set_request_context' ] );
The code above hooks a custom filtering function to the request context values. Let’s implement this function in the component below the constructor:
/**
* Sets request context for pages.
*
* @param array $context Context values.
* @return array
*/
public function set_request_context( $context ) {
// Get user ID.
$user_id = get_current_user_id();
// Get cached vendor IDs.
$vendor_ids = hivepress()->cache->get_user_cache( $user_id, 'vendor_follow_ids', 'models/follow' );
if ( is_null( $vendor_ids ) ) {
// Get follows.
$follows = Models\Follow::query()->filter(
[
'user' => $user_id,
]
)->get();
// Get vendor IDs.
$vendor_ids = [];
foreach ( $follows as $follow ) {
$vendor_ids[] = $follow->get_vendor__id();
}
// Cache vendor IDs.
hivepress()->cache->set_user_cache( $user_id, 'vendor_follow_ids', 'models/follow', $vendor_ids );
}
// Set request context.
$context['vendor_follow_ids'] = $vendor_ids;
return $context;
}
This function checks if the followed vendor IDs are cached for the current user, and if not, it queries the Follow model objects by user ID and fills an array of vendor IDs, then caches this array. Finally, it sets an array of vendor IDs in the vendor_follow_ids context, this allows us to get it anywhere in the code this way:
$vendor_ids = hivepress()->request->get_context( 'vendor_follow_ids' );
Also, the listing feed page we created doesn’t have any links on the front-end yet, so let’s add it to the user account menu. Add this code to the component constructor:
add_filter( 'hivepress/v1/menus/user_account', [ $this, 'add_menu_item' ] );
The code above hooks a custom filtering function to the user account menu parameters. Next, implement this function in the component below the constructor:
/**
* Adds menu item to user account.
*
* @param array $menu Menu arguments.
* @return array
*/
public function add_menu_item( $menu ) {
if ( hivepress()->request->get_context( 'vendor_follow_ids' ) ) {
$menu['items']['listings_feed'] = [
'route' => 'listings_feed_page',
'_order' => 20,
];
}
return $menu;
}
As you can see, it adds a custom menu item linked to the listings_feed_page route we created previously. You can adjust the _order parameter value to change the menu item position. The menu item will appear only if the current user follows any vendors.

Add REST API routes

Let’s also create URL routes that allow users to follow or unfollow vendors. Since these routes don’t render anything and are used for performing actions only, we will define them as REST API routes. These routes don’t require the title, redirect, and paginated parameters, but other parameters are needed instead:
  • method - restricts the accepted HTTP method (e.g. GET, POST);
  • rest - flag required for REST API routes.
It’s good practice to follow the {entity}-{context}-{type} pattern for naming routes (e.g. listing_view_page, vendor_update_action).
'vendor_follow_action' => [
'base' => 'vendor_resource',
'path' => '/follow',
'method' => 'POST',
'action' => [ $this, 'follow_vendor' ],
'rest' => true,
],
'vendors_unfollow_action' => [
'base' => 'vendors_resource',
'path' => '/unfollow',
'method' => 'POST',
'action' => [ $this, 'unfollow_vendors' ],
'rest' => true,
],
The code above defines 2 REST API routes, both accept requests via the POST method. The first route will follow or unfollow a vendor on every subsequent request, while the second one will unfollow all vendors at once. Next, implement the action functions for these routes:
/**
* Follows or unfollows vendor.
*
* @param WP_REST_Request $request API request.
* @return WP_Rest_Response
*/
public function follow_vendor( $request ) {
// Check authentication.
if ( ! is_user_logged_in() ) {
return hp\rest_error( 401 );
}
// Get vendor.
$vendor = Models\Vendor::query()->get_by_id( $request->get_param( 'vendor_id' ) );
if ( ! $vendor || $vendor->get_status() !== 'publish' ) {
return hp\rest_error( 404 );
}
// Get follows.
$follows = Models\Follow::query()->filter(
[
'user' => get_current_user_id(),
'vendor' => $vendor->get_id(),
]
)->get();
if ( $follows->count() ) {
// Delete follows.
$follows->delete();
} else {
// Add new follow.
$follow = ( new Models\Follow() )->fill(
[
'user' => get_current_user_id(),
'vendor' => $vendor->get_id(),
]
);
if ( ! $follow->save() ) {
return hp\rest_error( 400, $follow->_get_errors() );
}
}
return hp\rest_response(
200,
[
'data' => [],
]
);
}
/**
* Unfollows all vendors.
*
* @param WP_REST_Request $request API request.
* @return WP_Rest_Response
*/
public function unfollow_vendors( $request ) {
// Check authentication.
if ( ! is_user_logged_in() ) {
return hp\rest_error( 401 );
}
// Delete follows.
$follows = Models\Follow::query()->filter(
[
'user' => get_current_user_id(),
]
)->delete();
return hp\rest_response(
200,
[
'data' => [],
]
);
}
The follow_vendor function checks if the current user is logged in, gets a Vendor object by ID, and queries the Follow objects by the user and vendor IDs.
Then, if any Follow objects are found, they are deleted. If not, a new Follow object is created and saved in the database. This way, every subsequent call of this function will follow or unfollow a vendor, creating or deleting a Follow object.
The unfollow_vendors function checks if the current user is logged in, then queries the Follow objects by the user ID and deletes them, thus unfollowing all vendors.
Now we have all the URL routes and actions according to the extension requirements. You can view the complete controller source code on GitHub for reference.

Create a block

We created REST API routes, but there are no links or forms on the front-end that send requests to these routes yet. Let’s add a toggle link that sends a request to the vendor_follow_action on click and add it somewhere on the vendor page. We need to create a new block type for this.
In HivePress, block types are defined as PHP classes with properties and methods that determine the behavior and rendering of the block.
Create a class-follow-toggle.php file in the includes/blocks extension subdirectory. The file and PHP class naming conventions are the same as for components.
<?php
namespace HivePress\Blocks;
use HivePress\Helpers as hp;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* Block class.
*/
class Follow_Toggle extends Toggle {
/**
* Class constructor.
*
* @param array $args Block arguments.
*/
public function __construct( $args = [] ) {
$args = hp\merge_arrays(
[
'states' => [
[
'icon' => 'user-plus',
'caption' => esc_html__( 'Follow', 'foo-followers' ),
],
[
'icon' => 'user-minus',
'caption' => esc_html__( 'Unfollow', 'foo-followers' ),
],
],
],
$args
);
parent::__construct( $args );
}
/**
* Bootstraps block properties.
*/
protected function boot() {
// Get vendor from the block context.
$vendor = $this->get_context( 'vendor' );
if ( $vendor ) {
// Set URL for sending requests on click.
$this->url = hivepress()->router->get_url(
'vendor_follow_action',
[
'vendor_id' => $vendor->get_id(),
]
);
// Set active state if vendor is followed.
if ( in_array(
$vendor->get_id(),
hivepress()->request->get_context( 'vendor_follow_ids', [] )
) ) {
$this->active = true;
}
}
parent::boot();
}
}
Notice that the block class is based on the Toggle class – this is an existing block type available in HivePress, so all the properties and methods are inherited from it.
The Follow_Toggle block type we created defines 2 states for the toggle, setting the Font Awesome icon name and a label for each state.
Before the block is rendered, it fetches the Vendor object from the current template context and sets the toggle url to the vendor_follow_action route we created previously. It also enables the active flag if the vendor ID is among the followed vendor IDs.
This way, the toggle will show the “Follow” or “Unfollow” label on every subsequent click and send an AJAX request to the vendor_follow_action route URL. Also, it will show the “Unfollow” label by default if the vendor is already followed.
It’s good practice to re-use the existing HivePress block types and avoid creating new ones if possible. In this case, we had to create a new block type because it implements a custom logic (the toggle state depends on the followed vendor IDs).

Update the component

Next, let’s add this block to the vendor templates. Add this code to the component constructor:
add_filter( 'hivepress/v1/templates/vendor_view_block', [ $this, 'add_toggle_block' ] );
add_filter( 'hivepress/v1/templates/vendor_view_page', [ $this, 'add_toggle_block' ] );
The code above hooks a custom filtering function to the vendor template parameters. Now, implement this function in the component below the constructor:
/**
* Adds toggle block to vendor templates.
*
* @param array $template Template arguments.
* @return array
*/
public function add_toggle_block( $template ) {
return hp\merge_trees(
$template,
[
'blocks' => [
'vendor_actions_primary' => [
'blocks' => [
'vendor_follow_toggle' => [
'type' => 'follow_toggle',
'_order' => 50,
'attributes' => [
'class' => [ 'hp-vendor__action', 'hp-vendor__action--follow' ],
],
],
],
],
],
]
);
}
The function above filters the template parameters and adds a new block using the block type we created earlier. Let’s check if the toggle link is added on the front-end:
Now you can try clicking on the Follow toggle and check if the vendor listings appear on the listing feed page, and unfollow a vendor to check if listings disappear.

Create a form

We still have one action left that is not used anywhere, the one that unfollows all vendors at once. So let’s create a form for it and add a button to show the form on click.
Create a class-vendors-unfollow.php file in the includes/forms extension subdirectory. The file and PHP class naming conventions are the same as for components.
<?php
namespace HivePress\Forms;
use HivePress\Helpers as hp;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* Form class.
*/
class Vendors_Unfollow extends Form {
/**
* Class constructor.
*
* @param array $args Form arguments.
*/
public function __construct( $args = [] ) {
$args = hp\merge_arrays(
[
'description' => esc_html__( 'Are you sure you want to unfollow all vendors?', 'foo-followers' ),
'action' => hivepress()->router->get_url( 'vendors_unfollow_action' ),
'method' => 'POST',
'redirect' => true,
'button' => [
'label' => esc_html__( 'Unfollow', 'foo-followers' ),
],
],
$args
);
parent::__construct( $args );
}
}
The form we’ve created defines these parameters:
  • description - text displayed before the form;
  • action - URL for sending requests on submission;
  • method - HTTP method for sending requests (e.g. POST, GET);
  • redirect - flag to refresh or redirect the page;
  • button - the submit button parameters.
This form doesn’t contain fields, but you can define an array of fields in the fields parameter and the form will render them, sending the entered values with the request.

Update the template

Next, let’s add new blocks to the Listings_Feed_Page template we created earlier:
'vendors_unfollow_link' => [
'type' => 'part',
'path' => 'vendor/follow/vendors-unfollow-link',
'_order' => 30,
],
'vendors_unfollow_modal' => [
'title' => esc_html__( 'Unfollow Vendors', 'foo-followers' ),
'type' => 'modal',
'blocks' => [
'vendors_unfollow_form' => [
'type' => 'form',
'form' => 'vendors_unfollow',
],
],
],
As you can see, there’s a part block that loads a specific HTML file and a modal block that contains the form we’ve just created. The part block points to a non-existing file, so we need to create a vendors-unfollow-link.php file in the templates/vendor/follow extension subdirectory:
<?php
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
?>
<a href="#vendors_unfollow_modal" class="button"><?php esc_html_e( 'Unfollow Vendors', 'foo-followers' ); ?></a>
It’s good practice to follow the {entity}/{context}/{layout} directory structure for template parts, this way you can easily find the template where the part is used.
Now, let’s check the listing feed page. It should have the “Unfollow” button that opens a modal window on click. The modal window contains a form that sends a request to the vendors_unfollow_action route URL, thus unfollowing all vendors at once.

Create an email

Finally, let’s add an email notification sent to users about new listings from the followed vendors.
Create a class-listing-feed.php file in the includes/emails extension subdirectory. The file and PHP class naming conventions are the same as for components.
<?php
namespace HivePress\Emails;
use HivePress\Helpers as hp;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
/**
* Email class.
*/
class Listing_Feed extends Email {
/**
* Class constructor.
*
* @param array $args Email arguments.
*/
public function __construct( $args = [] ) {
$args = hp\merge_arrays(
[
'subject' => esc_html__( 'New Listing', 'foo-followers' ),
'body' => esc_html__( 'Hi, %user_name%! There is a new listing "%listing_title%" in your feed, click on the following link to view it: %listing_url%', 'foo-followers' ),
],
$args
);
parent::__construct( $args );
}
}
The email we’ve just created defines these parameters:
  • subject - the email subject;
  • body - the email message with placeholders.

Update the component

Next, let’s add a function that sends an email to the vendor followers if there’s a newly published listing. Add this code to the component constructor:
add_action( 'hivepress/v1/models/listing/update_status', [ $this, 'send_feed_emails' ], 10, 4 );
The code above hooks a custom function to the listing status change action. Now, implement this function in the component below the constructor:
/**
* Sends emails about a new listing.
*
* @param int $listing_id Listing ID.
* @param string $new_status New status.
* @param string $old_status Old status.
* @param object $listing Listing object.
*/
public function send_feed_emails( $listing_id, $new_status, $old_status, $listing ) {
// Check listing status.
if ( 'publish' !== $new_status || ! in_array( $old_status, [ 'auto-draft', 'pending' ] ) ) {
return;
}
// Get follows.
$follows = Models\Follow::query()->filter(
[
'vendor' => $listing->get_vendor__id(),
]
)->get();
foreach ( $follows as $follow ) {
// Get user.
$user = $follow->get_user();
// Send email.
( new Emails\Listing_Feed(
[
'recipient' => $user->get_email(),
'tokens' => [
'user_name' => $user->get_display_name(),
'listing_title' => $listing->get_title(),
'listing_url' => hivepress()->router->get_url( 'listing_view_page', [ 'listing_id' => $listing->get_id() ] ),
],
]
) )->send();
}
}
The function above checks if the listing got the "published" status, gets all the Follow objects by vendor ID and sends an email to each follower, providing the email address and tokens to be replaced in the email text.
Now, if you follow a vendor and this vendor publishes a new listing, you will get an email notification that contains the listing title and URL.

Create a POT file

You probably noticed that we wrapped all the texts in the code with the translation functions. We also need to generate a POT file for translation to work properly.
Create a new languages extension subdirectory and install the Loco Translate plugin. Go to Loco Translate > Plugins > Followers for HivePress and click Create template, then proceed.
That's it. Now website owners can translate or change any of the extension texts via Loco Translate or POEdit without editing the source code directly.

Keep developing

Congratulations! You’ve just developed a fully-functional HivePress extension. Even though there's a lot more to the HivePress framework than what you’ve seen so far, you’re now ready to start developing your own HivePress extensions.
For example, you can develop a custom HivePress extension for a client, share it on the WordPress.org repository or even sell it on the CodeCanyon marketplace.
If you have any questions about the HivePress framework, please check the available docs and feel free to join the HivePress developer community.