namespace Yoast\WP\SEO\Integrations\Third_Party;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Conditionals\WooCommerce_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Pagination_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Memoizers\Meta_Tags_Context_Memoizer;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Presentations\Indexable_Presentation;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
* WooCommerce integration.
class WooCommerce implements Integration_Interface {
* The WPSEO Replace Vars object.
* @var WPSEO_Replace_Vars
* The memoizer for the meta tags context.
* @var Meta_Tags_Context_Memoizer
protected $context_memoizer;
* The indexable repository.
* @var Indexable_Repository
protected $pagination_helper;
* Returns the conditionals based in which this loadable should be active.
public static function get_conditionals() {
return [ WooCommerce_Conditional::class, Front_End_Conditional::class ];
* WooCommerce constructor.
* @param Options_Helper $options The options helper.
* @param WPSEO_Replace_Vars $replace_vars The replace vars helper.
* @param Meta_Tags_Context_Memoizer $context_memoizer The meta tags context memoizer.
* @param Indexable_Repository $repository The indexable repository.
* @param Pagination_Helper $pagination_helper The paginataion helper.
public function __construct(
WPSEO_Replace_Vars $replace_vars,
Meta_Tags_Context_Memoizer $context_memoizer,
Indexable_Repository $repository,
Pagination_Helper $pagination_helper
$this->options = $options;
$this->replace_vars = $replace_vars;
$this->context_memoizer = $context_memoizer;
$this->repository = $repository;
$this->pagination_helper = $pagination_helper;
* Initializes the integration.
* This is the place to register hooks and filters.
public function register_hooks() {
\add_filter( 'wpseo_frontend_page_type_simple_page_id', [ $this, 'get_page_id' ] );
\add_filter( 'wpseo_breadcrumb_indexables', [ $this, 'add_shop_to_breadcrumbs' ] );
\add_filter( 'wpseo_title', [ $this, 'title' ], 10, 2 );
\add_filter( 'wpseo_metadesc', [ $this, 'description' ], 10, 2 );
\add_filter( 'wpseo_canonical', [ $this, 'canonical' ], 10, 2 );
\add_filter( 'wpseo_adjacent_rel_url', [ $this, 'adjacent_rel_url' ], 10, 3 );
* Returns the correct canonical when WooCommerce is enabled.
* @param string $canonical The current canonical.
* @param Indexable_Presentation|null $presentation The indexable presentation.
* @return string The correct canonical.
public function canonical( $canonical, $presentation = null ) {
if ( ! $this->is_shop_page() ) {
$url = $this->get_shop_paginated_link( 'curr', $presentation );
* Returns correct adjacent pages when WooCommerce is enabled.
* @param string $link The current link.
* @param string $rel Link relationship, prev or next.
* @param Indexable_Presentation|null $presentation The indexable presentation.
* @return string The correct link.
public function adjacent_rel_url( $link, $rel, $presentation = null ) {
if ( ! $this->is_shop_page() ) {
if ( $rel !== 'next' && $rel !== 'prev' ) {
$url = $this->get_shop_paginated_link( $rel, $presentation );
* Adds a breadcrumb for the shop page.
* @param Indexable[] $indexables The array with indexables.
* @return Indexable[] The indexables to be shown in the breadcrumbs, with the shop page added.
public function add_shop_to_breadcrumbs( $indexables ) {
$shop_page_id = $this->get_shop_page_id();
if ( $shop_page_id < 1 ) {
foreach ( $indexables as $index => $indexable ) {
if ( $indexable->object_type === 'post-type-archive' && $indexable->object_sub_type === 'product' ) {
$indexables[ $index ] = $this->repository->find_by_id_and_type( $shop_page_id, 'post' );
* Returns the ID of the WooCommerce shop page when the currently opened page is the shop page.
* @param int $page_id The page id.
* @return int The Page ID of the shop.
public function get_page_id( $page_id ) {
if ( ! $this->is_shop_page() ) {
return $this->get_shop_page_id();
* @param string $title The title.
* @param Indexable_Presentation|null $presentation The indexable presentation.
* @return string The title to use.
public function title( $title, $presentation = null ) {
$presentation = $this->ensure_presentation( $presentation );
if ( $presentation->model->title ) {
if ( ! $this->is_shop_page() ) {
$shop_page_id = $this->get_shop_page_id();
if ( $shop_page_id < 1 ) {
$product_template_title = $this->get_product_template( 'title-product', $shop_page_id );
if ( $product_template_title ) {
return $product_template_title;
* Handles the meta description.
* @param string $description The title.
* @param Indexable_Presentation|null $presentation The indexable presentation.
* @return string The description to use.
public function description( $description, $presentation = null ) {
$presentation = $this->ensure_presentation( $presentation );
if ( $presentation->model->description ) {
if ( ! $this->is_shop_page() ) {
$shop_page_id = $this->get_shop_page_id();
if ( $shop_page_id < 1 ) {
$product_template_description = $this->get_product_template( 'metadesc-product', $shop_page_id );
if ( $product_template_description ) {
return $product_template_description;
* Checks if the current page is a WooCommerce shop page.
* @return bool True when the page is a shop page.
protected function is_shop_page() {
* Uses template for the given option name and replace the replacement variables on it.
* @param string $option_name The option name to get the template for.
* @param string $shop_page_id The page id to retrieve template for.
* @return string The rendered value.
protected function get_product_template( $option_name, $shop_page_id ) {
$template = $this->options->get( $option_name );
$page = \get_post( $shop_page_id );
return $this->replace_vars->replace( $template, $page );
* Returns the id of the set WooCommerce shop page.
* @return int The ID of the set page.
protected function get_shop_page_id() {
if ( ! \function_exists( 'wc_get_page_id' ) ) {
return \wc_get_page_id( 'shop' );
* Get paginated link for shop page.
* @param string $rel Link relationship, prev or next or curr.
* @param Indexable_Presentation|null $presentation The indexable presentation.
* @return string|null The link.
protected function get_shop_paginated_link( $rel, $presentation = null ) {
$presentation = $this->ensure_presentation( $presentation );
$permalink = $presentation->get_permalink();
$current_page = \max( 1, $this->pagination_helper->get_current_archive_page_number() );
if ( $rel === 'curr' && $current_page === 1 ) {
if ( $rel === 'curr' && $current_page > 1 ) {
return $this->pagination_helper->get_paginated_url( $permalink, $current_page );
if ( $rel === 'prev' && $current_page === 2 ) {
if ( $rel === 'prev' && $current_page > 2 ) {
return $this->pagination_helper->get_paginated_url( $permalink, ( $current_page - 1 ) );
if ( $rel === 'next' && $current_page < $this->pagination_helper->get_number_of_archive_pages() ) {
return $this->pagination_helper->get_paginated_url( $permalink, ( $current_page + 1 ) );
* Ensures a presentation is available.
* @param Indexable_Presentation $presentation The indexable presentation.
* @return Indexable_Presentation The presentation, taken from the current page if the input was invalid.
protected function ensure_presentation( $presentation ) {
if ( \is_a( $presentation, Indexable_Presentation::class ) ) {
$context = $this->context_memoizer->for_current_page();
return $context->presentation;