WooCommerce My Account Tab Generator

Add a tab to the WooCommerce My Account page: the rewrite endpoint, the query var, the menu item in the right position and the content callback.

Live output

Enable JavaScript to customise; default output below.

Becomes the URL: /my-account/rewards/. It is also half of every hook name below.

Used for the browser title on that tab. Leave empty to keep the menu label.

Position in the menu

Logout should stay last. Appending blindly puts your tab under it.

Live preview my-account-tab.php
<?php
/**
 * The "Rewards" tab on the My Account page.
 */

/**
 * Registers the endpoint. This is what makes /my-account/rewards/ resolve.
 */
function my_store_add_endpoint() {
	add_rewrite_endpoint( 'rewards', EP_ROOT | EP_PAGES );
}
add_action( 'init', 'my_store_add_endpoint' );

/**
 * Tells WordPress the endpoint is a query var it should keep.
 *
 * @param array $vars Public query vars.
 * @return array
 */
function my_store_query_vars( $vars ) {
	$vars[] = 'rewards';

	return $vars;
}
add_filter( 'query_vars', 'my_store_query_vars', 0 );

/**
 * Adds the menu item.
 *
 * @param array $items Menu items, keyed by endpoint.
 * @return array
 */
function my_store_menu_items( $items ) {
	$new = array(
		'rewards' => __( 'Rewards', 'my-store' ),
	);

	// Logout belongs at the end, so lift it off, append, and put it back.
	$logout = array();

	if ( isset( $items['customer-logout'] ) ) {
		$logout['customer-logout'] = $items['customer-logout'];

		unset( $items['customer-logout'] );
	}

	return array_merge( $items, $new, $logout );
}
add_filter( 'woocommerce_account_menu_items', 'my_store_menu_items' );

/**
 * The browser title on that tab.
 *
 * @param string $title Current title.
 * @return string
 */
function my_store_endpoint_title( $title ) {
	return __( 'Your rewards', 'my-store' );
}
add_filter( 'woocommerce_endpoint_rewards_title', 'my_store_endpoint_title' );

/**
 * Prints the tab content.
 *
 * The hook name carries the endpoint slug, so renaming the slug renames this
 * hook too.
 */
function my_store_endpoint_content() {
	echo '<p>' . esc_html__( 'Everything you have earned so far.', 'my-store' ) . '</p>';

	// Your content here.
}
add_action( 'woocommerce_account_rewards_endpoint', 'my_store_endpoint_content' );

/**
 * Registers the endpoint and rewrites the rules once, on activation.
 *
 * Without this the URL is a 404 until someone opens Settings, Permalinks.
 */
function my_store_activate() {
	my_store_add_endpoint();

	flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_store_activate' );

/**
 * Clears the rule again so the URL stops resolving.
 */
function my_store_deactivate() {
	flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'my_store_deactivate' );

Output is valid and updates as you type.

Add your own tab to WooCommerce My Account. The generator writes all four pieces that have to agree: the rewrite endpoint, the query var, the menu item and the content hook.

How to use

  1. Pick the endpoint slug first. It becomes the URL, the menu key and half of two hook names, so changing it later means changing all of them.
  2. Put the tab before Logout. Logout belongs at the end of the list, and appending blindly puts your tab underneath it.
  3. Choose a template file if a theme should be able to override the content. wc_get_template() is how WooCommerce does its own tabs.
  4. Leave the activation flush on. A new endpoint is a 404 until the rewrite rules are rebuilt.
  5. If the tab 404s after you change the slug, open Settings, Permalinks and save. That rebuilds the rules by hand.

Example

The menu item, inserted before Logout rather than after it:

function acme_menu_items( $items ) {
	$new = array(
		'rewards' => __( 'Rewards', 'acme' ),
	);

	$logout = array();

	if ( isset( $items['customer-logout'] ) ) {
		$logout['customer-logout'] = $items['customer-logout'];

		unset( $items['customer-logout'] );
	}

	return array_merge( $items, $new, $logout );
}
add_filter( 'woocommerce_account_menu_items', 'acme_menu_items' );

The array key has to match the endpoint slug exactly. A mismatch gives you a menu item that links nowhere, or a working URL with no link to it.

Pitfalls

  • flush_rewrite_rules() on every request is expensive. Call it on activation, never on init.
  • Four things use the slug: the endpoint, the query var, the menu key and the content hook. Three out of four gives you a tab that half works.
  • Appending the item without moving Logout puts your tab after it, which every customer reads as the end of the list.
  • The endpoint only exists on the My Account page. EP_ROOT | EP_PAGES is what makes it work on the page WooCommerce assigned.
  • The content hook prints inside the account wrapper. Echoing a full page template gives you nested markup.
  • Plugins that cache pages will cache the tab. Content that varies per customer needs to be excluded from caching.
  • Deactivating without a flush leaves the rewrite rule behind until permalinks are saved again.
  • A slug that collides with an existing WooCommerce endpoint, such as orders or downloads, silently overrides it.

Compatibility

add_rewrite_endpoint() is WordPress core and has been stable since 3.4. woocommerce_account_menu_items, woocommerce_account_<endpoint>_endpoint and woocommerce_endpoint_<endpoint>_title have been stable since WooCommerce 2.6 and still apply in WooCommerce 8 and 9. My Account has no block equivalent, so these hooks remain the way in. The generated code targets PHP 7.0 and up, and the tool runs entirely in your browser.

Frequently asked questions

Why is my tab a 404?
The rewrite rules have not been rebuilt. Reactivate the plugin, or save Settings, Permalinks once.
How do I put the tab in a specific position?
Rebuild the array in the order you want. The generated code shows the three common cases, including keeping Logout last.
Can a theme override the content?
Yes, if you use wc_get_template(). A theme can then place the same path under its own woocommerce folder.
How do I restrict the tab to some customers?
Return $items unchanged for everyone else in the menu filter, and check again in the content callback. The URL is guessable, so both checks matter.
Does this work with the checkout and cart blocks?
Those are separate. My Account is still rendered by the shortcode and these hooks.

From the people who built this tool

WP Adminify

The WordPress admin, rebuilt: a dashboard worth looking at, menu and column control, a real file manager and the login page your client sees.

See WP Adminify Free version on WordPress.org

Weekly drops

New tools, when there are new tools

One email when something worth using ships. No schedule to fill, so no filler.

Your address goes nowhere else, and one click unsubscribes.