diff --git a/Model/Indexer/Category/CategoryIndexer.php b/Model/Indexer/Category/CategoryIndexer.php
new file mode 100644
index 0000000..1634db7
--- /dev/null
+++ b/Model/Indexer/Category/CategoryIndexer.php
@@ -0,0 +1,153 @@
+dimensionProvider = $dimensionProvider;
+ $this->getUrlsForCategories = $getUrlsForCategories;
+ $this->prerenderClient = $prerenderClient;
+ $this->deploymentConfig = $deploymentConfig;
+ $this->batchSize = $batchSize;
+ $this->prerenderConfigHelper = $prerenderConfigHelper;
+ }
+
+ /**
+ * Execute full indexation
+ *
+ * @return void
+ */
+ public function executeFull(): void
+ {
+ $this->executeList([]);
+ }
+
+ /**
+ * Execute partial indexation by ID list
+ *
+ * @param int[] $ids
+ * @return void
+ */
+ public function executeList(array $ids): void
+ {
+ foreach ($this->dimensionProvider->getIterator() as $dimension) {
+ try {
+ $this->executeByDimensions($dimension, new \ArrayIterator($ids));
+ } catch (FileSystemException|RuntimeException $e) {
+ continue;
+ }
+ }
+ }
+
+ /**
+ * Execute partial indexation by ID
+ *
+ * @param int $id
+ * @return void
+ * @throws LocalizedException
+ */
+ public function executeRow($id): void
+ {
+ if (!$id) {
+ throw new LocalizedException(
+ __('Cannot recache url for an undefined product.')
+ );
+ }
+ $this->executeList([$id]);
+ }
+
+ /**
+ * Execute materialization on ids entities
+ *
+ * @param int[] $ids
+ * @return void
+ */
+ public function execute($ids): void
+ {
+ $this->executeList($ids);
+ }
+
+ /**
+ * Execute indexing per dimension (store)
+ *
+ * @param arry $dimensions
+ * @param \Traversable $entityIds
+ * @throws FileSystemException
+ * @throws RuntimeException
+ */
+ public function executeByDimensions(array $dimensions, \Traversable $entityIds): void
+ {
+ if (count($dimensions) > 1 || !isset($dimensions[StoreDimensionProvider::DIMENSION_NAME])) {
+ throw new \InvalidArgumentException('Indexer "' . self::INDEXER_ID . '" supports only Store dimension');
+ }
+ $storeId = (int)$dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue();
+
+ if (!$this->prerenderConfigHelper->isRecacheEnabled($storeId)) {
+ return;
+ }
+
+ $entityIds = iterator_to_array($entityIds);
+ // get urls for the products
+ $urls = $this->getUrlsForCategories->execute($entityIds, $storeId);
+
+ $this->batchSize = $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . self::INDEXER_ID . '/partial_reindex'
+ ) ?? $this->batchSize;
+
+ $urlBatches = array_chunk($urls, $this->batchSize);
+ foreach ($urlBatches as $batchUrls) {
+ $this->prerenderClient->recacheUrls($batchUrls, $storeId);
+ }
+ }
+}
diff --git a/Model/Indexer/Category/ProductIndexer.php b/Model/Indexer/Category/ProductIndexer.php
new file mode 100644
index 0000000..696dd99
--- /dev/null
+++ b/Model/Indexer/Category/ProductIndexer.php
@@ -0,0 +1,162 @@
+dimensionProvider = $dimensionProvider;
+ $this->productCategoriesDataProvider = $productCategoriesDataProvider;
+ $this->getUrlsForCategories = $getUrlsForCategories;
+ $this->prerenderClient = $prerenderClient;
+ $this->deploymentConfig = $deploymentConfig;
+ $this->batchSize = $batchSize;
+ $this->prerenderConfigHelper = $prerenderConfigHelper;
+ }
+
+ /**
+ * Execute full indexation
+ *
+ * @return void
+ */
+ public function executeFull(): void
+ {
+ $this->executeList([]);
+ }
+
+ /**
+ * Execute partial indexation by ID list
+ *
+ * @param int[] $ids
+ * @return void
+ */
+ public function executeList(array $ids): void
+ {
+ foreach ($this->dimensionProvider->getIterator() as $dimension) {
+ try {
+ $this->executeByDimensions($dimension, new \ArrayIterator($ids));
+ } catch (FileSystemException|RuntimeException $e) {
+ continue;
+ }
+ }
+ }
+
+ /**
+ * Execute partial indexation by ID
+ *
+ * @param int $id
+ * @return void
+ * @throws LocalizedException
+ */
+ public function executeRow($id): void
+ {
+ if (!$id) {
+ throw new LocalizedException(
+ __('Cannot recache url for an undefined product.')
+ );
+ }
+ $this->executeList([$id]);
+ }
+
+ /**
+ * Execute materialization on ids entities
+ *
+ * @param int[] $ids
+ * @return void
+ */
+ public function execute($ids): void
+ {
+ $this->executeList($ids);
+ }
+
+ /**
+ * Execute indexing per dimension (store)
+ *
+ * @param arry $dimensions
+ * @param \Traversable $entityIds
+ * @throws FileSystemException
+ * @throws RuntimeException
+ */
+ public function executeByDimensions(array $dimensions, \Traversable $entityIds): void
+ {
+ if (count($dimensions) > 1 || !isset($dimensions[StoreDimensionProvider::DIMENSION_NAME])) {
+ throw new \InvalidArgumentException('Indexer "' . self::INDEXER_ID . '" supports only Store dimension');
+ }
+ $storeId = (int)$dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue();
+
+ if (!$this->prerenderConfigHelper->isRecacheEnabled($storeId)) {
+ return;
+ }
+
+ $entityIds = iterator_to_array($entityIds);
+ // get list of category ids for the products
+ $categoryIds = $this->productCategoriesDataProvider->getCategoryIdsForProducts($entityIds, $storeId);
+
+ // get urls for the products
+ $urls = $this->getUrlsForCategories->execute($categoryIds, $storeId);
+
+ $this->batchSize = $this->deploymentConfig->get(
+ self::DEPLOYMENT_CONFIG_INDEXER_BATCHES . self::INDEXER_ID . '/partial_reindex'
+ ) ?? $this->batchSize;
+
+ $urlBatches = array_chunk($urls, $this->batchSize);
+ foreach ($urlBatches as $batchUrls) {
+ $this->prerenderClient->recacheUrls($batchUrls, $storeId);
+ }
+ }
+}
diff --git a/Model/Indexer/DataProvider/ProductCategories.php b/Model/Indexer/DataProvider/ProductCategories.php
new file mode 100644
index 0000000..8b3acb1
--- /dev/null
+++ b/Model/Indexer/DataProvider/ProductCategories.php
@@ -0,0 +1,62 @@
+productCollectionFactory = $productCollectionFactory;
+ $this->categoryCollectionFactory = $categoryCollectionFactory;
+ }
+
+ /**
+ * Get complete list of categories containing one or more of the given products
+ *
+ * @param array $productIds
+ * @param int $storeId
+ * @return array
+ */
+ public function getCategoryIdsForProducts(array $productIds, int $storeId): array
+ {
+ // if array of product ids is empty, just load all categories
+ if (empty($productIds)) {
+ $categoryCollection = $this->categoryCollectionFactory->create();
+ $categoryCollection->setStoreId($storeId);
+ return $categoryCollection->getAllIds();
+ }
+
+ $productCollection = $this->productCollectionFactory->create();
+ $productCollection->addIdFilter($productIds);
+ $productCollection->setStoreId($storeId);
+ // add category information
+ $productCollection->addCategoryIds();
+
+ $categoryIds = [];
+ /** @var Product $product */
+ foreach ($productCollection->getItems() as $product) {
+ $categoryIds[] = $product->getCategoryIds();
+ }
+ return array_unique(array_merge(...$categoryIds));
+ }
+}
diff --git a/Model/Indexer/Product/ProductIndexer.php b/Model/Indexer/Product/ProductIndexer.php
index 4134383..83f3cc7 100644
--- a/Model/Indexer/Product/ProductIndexer.php
+++ b/Model/Indexer/Product/ProductIndexer.php
@@ -9,7 +9,7 @@
use Aligent\PrerenderIo\Api\PrerenderClientInterface;
use Aligent\PrerenderIo\Helper\Config;
-use Aligent\PrerenderIo\Model\Product\GetUrlsForProducts;
+use Aligent\PrerenderIo\Model\Url\GetUrlsForProducts;
use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\LocalizedException;
diff --git a/Model/Url/GetUrlsForCategories.php b/Model/Url/GetUrlsForCategories.php
new file mode 100644
index 0000000..50d9bfb
--- /dev/null
+++ b/Model/Url/GetUrlsForCategories.php
@@ -0,0 +1,83 @@
+categoryCollectionFactory = $categoryCollectionFactory;
+ $this->storeManager = $storeManager;
+ $this->emulation = $emulation;
+ }
+
+ /**
+ * Generate category URLs based on URL_REWRITE entries
+ *
+ * @param array $categoryIds
+ * @param int $storeId
+ * @return array
+ */
+ public function execute(array $categoryIds, int $storeId): array
+ {
+ $categoryCollection = $this->categoryCollectionFactory->create();
+ // if array of category ids is empty, just load all categories
+ if (!empty($categoryIds)) {
+ $categoryCollection->addIdFilter($categoryIds);
+ }
+ $categoryCollection->setStoreId($storeId);
+ $categoryCollection->addUrlRewriteToResult();
+
+ try {
+ /** @var Store $store */
+ $store = $this->storeManager->getStore($storeId);
+ } catch (NoSuchEntityException $e) {
+ return [];
+ }
+
+ $this->emulation->startEnvironmentEmulation($storeId);
+ $urls = [];
+ /** @var Category $category */
+ foreach ($categoryCollection as $category) {
+ $urlPath = $category->getData('request_path');
+ if (empty($urlPath)) {
+ continue;
+ }
+ try {
+ // remove trailing slashes from urls
+ $urls[] = rtrim($store->getUrl($urlPath), '/');
+ } catch (NoSuchEntityException $e) {
+ continue;
+ }
+ }
+ $this->emulation->stopEnvironmentEmulation();
+ return $urls;
+ }
+}
diff --git a/Model/Product/GetUrlsForProducts.php b/Model/Url/GetUrlsForProducts.php
similarity index 98%
rename from Model/Product/GetUrlsForProducts.php
rename to Model/Url/GetUrlsForProducts.php
index f89d498..bfec571 100644
--- a/Model/Product/GetUrlsForProducts.php
+++ b/Model/Url/GetUrlsForProducts.php
@@ -4,7 +4,7 @@
*/
declare(strict_types=1);
-namespace Aligent\PrerenderIo\Model\Product;
+namespace Aligent\PrerenderIo\Model\Url;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
diff --git a/README.md b/README.md
index 67b9b6a..bb31471 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,14 @@
# magento2-prerender-io
-Provides integration between Magento 2 and [Prerender.io](https://prerender.io), giving the ability for product pages to be automatically recached when a product is updated.
+Provides integration between Magento 2 and [Prerender.io](https://prerender.io), giving the ability for pages to be automatically recached when required.
## Overview
-This module provides a new indexer, `prerender_io_product`, which will send URL recache requests to Prerender.io (in batches of up to 1000) when changes are made to products.
-This will ensure that the cache product pages are kept up-to-date at all times.
+This module provides new indexers:
+
+- `prerender_io_product`, which will send URL recache requests for products to Prerender.io (in batches of up to 1000) when changes are made to products.
+- `prerender_io_category`, which will send URL recache requests for categories to Prerender.io (in batches of up to 1000) when changes are made to categories.
+- `prerender_io_category_product`, which will send URL recache requests for categories to Prerender.io (in batches of up to 1000) when changes are made to products.
+
+These will ensure that the cached pages are kept up-to-date at all times.
## Installation
To install via composer, simply run:
@@ -12,12 +17,12 @@ To install via composer, simply run:
composer require aligent/magento2-prerender-io
```
-Then, ensure the module is installed and the index is set to `Schedule`:
+Then, ensure the module is installed and the indexers are set to `Schedule`:
```bash
bin/magento module:enable Aligent_PrerenderIo
bin/magento setup:upgrade
-bin/magento indexer:set-mode schedule prerender_io_product
+bin/magento indexer:set-mode schedule prerender_io_product prerender_io_category prerender_io_category_product
```
## Configuration
diff --git a/etc/di.xml b/etc/di.xml
index d81f5fe..165a5a2 100644
--- a/etc/di.xml
+++ b/etc/di.xml
@@ -14,4 +14,18 @@
+