diff --git a/CHANGELOG.md b/CHANGELOG.md index 4823510cf..ae8110f3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # SEOmatic Changelog +## 5.1.1 - 2024.07.19 +### Changed +* Renamed the **Regenerate Sitemaps Automatically** setting to **Invalidate Sitemap Caches Automatically** for clarity + +### Fixed +* Fixed an issue where getting the sitemaps via GraphQL and meta container endpoint only retrieved the first page since the switch to paginated sitemaps ([#1492](https://github.com/nystudio107/craft-seomatic/issues/1492)) +* Fixed an issue where saving an entry could be slow, because SEOmatic was pointlessly trying to regenerate the sitemap cache (which is no longer a thing with paginated sitemaps) ([#1494](https://github.com/nystudio107/craft-seomatic/issues/1494)) + ## 5.1.0 - 2024.07.12 ### Added * Remove queue generated sitemaps, switch to paginated sitemaps to allow them to be rendered at web response time, but still be managable in size diff --git a/composer.json b/composer.json index 0096a778a..a6dad0ed1 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "nystudio107/craft-seomatic", "description": "SEOmatic facilitates modern SEO best practices & implementation for Craft CMS 5. It is a turnkey SEO system that is comprehensive, powerful, and flexible.", "type": "craft-plugin", - "version": "5.1.0", + "version": "5.1.1", "minimum-stability": "dev", "prefer-stable": true, "keywords": [ diff --git a/docs/docs/configuring/content-seo.md b/docs/docs/configuring/content-seo.md index 1f1fe856a..148f30dda 100644 --- a/docs/docs/configuring/content-seo.md +++ b/docs/docs/configuring/content-seo.md @@ -52,7 +52,7 @@ Search engines no longer accept [sitemap pings](https://developers.google.com/se SEOmatic generates sitemaps on demand when requested on the frontend, and then caches the sitemap for future requests so they are fast. -Normally SEOmatic will invalidate the cache for a sitemap for a Section, Category Group, or Product any time you save an element. However, if you prefer to regenerate the sitemap manually you can disable the **Regenerate Sitemaps Automatically** option in SEOmatic’s Plugin Settings. +Normally SEOmatic will invalidate the cache for a sitemap for a Section, Category Group, or Product any time you save an element. However, if you prefer to invalidate the sitemap caches manually you can disable the **Invalidate Sitemap Caches Automatically** option in SEOmatic’s Plugin Settings. ![Screenshot of a console running the following command to generate a blog sitemap: `./craft seomatic/sitemap/generate --siteId=1 --handle=blog`](../resources/screenshots/seomatic-sitemap-console-command.png) @@ -81,7 +81,7 @@ You can also limit it to a specific Section, Category Group, or Product handle: ``` ::: tip Manually Updating Sitemaps -If you disable **Regenerate Sitemaps Automatically**, sitemaps can _only_ be updated via CLI command, or by clearing SEOmatic’s sitemap caches via **Utilities** → **Clear Caches**. +If you disable **Invalidate Sitemap Caches Automatically**, sitemaps can _only_ be updated via CLI command, or by clearing SEOmatic’s sitemap caches via **Utilities** → **Clear Caches**. ::: ### Additional Sitemaps diff --git a/docs/docs/configuring/plugin-settings.md b/docs/docs/configuring/plugin-settings.md index bd29e2070..7a8fc4532 100644 --- a/docs/docs/configuring/plugin-settings.md +++ b/docs/docs/configuring/plugin-settings.md @@ -9,7 +9,7 @@ Plugin Settings let you control various SEOmatic settings across all sites/langu * **Plugin name** – The name used for the plugin throughout the control panel. * **Automatic Render Enabled** – Controls whether SEOmatic automatically renders metadata on your pages. If you turn this off, you will need to manually render the metadata via `seomatic.tag.render()`, `seomatic.link.render()`, etc. Selectively disable rendering via Twig with `{% do seomatic.config.renderEnabled(false) %}`. * **Sitemaps Enabled** – Controls whether SEOmatic will automatically render front-end sitemaps for your site. -* **Regenerate Sitemaps Automatically** – Controls whether sitemaps will automatically be regenerated when entries are saved. +* **Invalidate Sitemap Caches Automatically** – Controls whether sitemap caches will automatically be invalidated when entries are saved. * **Include Homepage in Breadcrumbs** – Should the homepage be included in the generated Breadcrumbs JSON-LD? * **Manually Set SEOmatic Environment** – If off, SEOmatic will automatically attempt to determine the current environment. Turn this on to manually set the environment. * **Environment** – The server environment, either `live`, `staging`, or `local`. If `devMode` is on, SEOmatic will override this setting to local Development. This setting controls whether certain things render; for instance only in the `live` production environment will Google Analytics and other tracking tags send analytics data. SEOmatic also automatically sets the `robots` tag to `none` for everything but the `live` production environment. diff --git a/src/gql/resolvers/SitemapResolver.php b/src/gql/resolvers/SitemapResolver.php index fce71e22a..05d46cf9c 100644 --- a/src/gql/resolvers/SitemapResolver.php +++ b/src/gql/resolvers/SitemapResolver.php @@ -15,9 +15,11 @@ use GraphQL\Type\Definition\ResolveInfo; use nystudio107\seomatic\helpers\Gql as GqlHelper; use nystudio107\seomatic\helpers\PluginTemplate; +use nystudio107\seomatic\helpers\Sitemap; use nystudio107\seomatic\models\SitemapCustomTemplate; use nystudio107\seomatic\models\SitemapIndexTemplate; use nystudio107\seomatic\models\SitemapTemplate; +use nystudio107\seomatic\Seomatic; use yii\web\NotFoundHttpException; /** @@ -50,13 +52,16 @@ public static function getSitemaps($source, array $arguments, $context, ResolveI return []; } + $sitemapList = []; // If either of the source bundle arguments are present, get the sitemap if (!empty($arguments['sourceBundleType']) || !empty($arguments['sourceBundleHandle'])) { - $filename = self::createFilenameFromComponents($site->groupId, $arguments['sourceBundleType'] ?? '', $arguments['sourceBundleHandle'] ?? '', $siteId); + $filenames = self::createSitemapFilenamesFromComponents($site->groupId, $arguments['sourceBundleType'] ?? '', $arguments['sourceBundleHandle'] ?? '', $siteId); - return [ - self::getSitemapItemByFilename($filename), - ]; + foreach ($filenames as $filename) { + $sitemapList[] = self::getSitemapItemByFilename($filename); + } + + return $sitemapList; } $sitemapIndexItems = [self::getSitemapIndexListEntry($siteId, $site->groupId)]; @@ -143,7 +148,7 @@ protected static function getSitemapIndexListEntry($siteId, $groupId): array */ protected static function getSitemapItemByFilename($filename) { - if (!preg_match('/sitemaps-(?P\d+)-(?P[\w\.*]+)-(?P[\w\.*]+)-(?P\d+)/i', $filename, $matches)) { + if (!preg_match('/sitemaps-(?P\d+)-(?P[\w\.*]+)-(?P[\w\.*]+)-(?P\d+)-sitemap(-p(?P\d+))?/i', $filename, $matches)) { return null; } @@ -163,10 +168,31 @@ protected static function getSitemapItemByFilename($filename) * @param string $bundleType * @param string $bundleHandle * @param int $siteId - * @return string + * @return array */ - protected static function createFilenameFromComponents(int $groupId, string $bundleType, string $bundleHandle, int $siteId): string + protected static function createSitemapFilenamesFromComponents(int $groupId, string $bundleType, string $bundleHandle, int $siteId): array { - return "sitemaps-$groupId-$bundleType-$bundleHandle-$siteId-sitemap.xml"; + $metaBundle = Seomatic::$plugin->metaBundles->getMetaBundleBySourceHandle($bundleType, $bundleHandle, $siteId); + if (!$metaBundle) { + return []; + } + + $pageSize = $metaBundle->metaSitemapVars->sitemapPageSize ?? null; + if (!$pageSize) { + return ["sitemaps-$groupId-$bundleType-$bundleHandle-$siteId-sitemap.xml"]; + } + + $sitemapFilenames = []; + $seoElement = Seomatic::$plugin->seoElements->getSeoElementByMetaBundleType($metaBundle->sourceBundleType); + if ($seoElement) { + $totalElements = Sitemap::getTotalElementsInSitemap($seoElement, $metaBundle); + $pageCount = $pageSize > 0 ? ceil($totalElements / $pageSize) : 1; + + for ($page = 1; $page <= $pageCount; $page++) { + $sitemapFilenames[] = sprintf('sitemaps-%d-%s-%s-%d-sitemap-p%d.xml', $groupId, $bundleType, $bundleHandle, $siteId, $page); + } + } + + return $sitemapFilenames; } } diff --git a/src/helpers/Sitemap.php b/src/helpers/Sitemap.php index 3d340eccb..d04ad3b65 100644 --- a/src/helpers/Sitemap.php +++ b/src/helpers/Sitemap.php @@ -411,6 +411,23 @@ public static function generateSitemap(array $params): ?string return implode('', $lines); } + /** + * Return the total number of elements in a sitemap, respecting metabundle settings. + * + * @param class-string $seoElement + * @param MetaBundle $metaBundle + * @return int|null + */ + public static function getTotalElementsInSitemap(string $seoElement, MetaBundle $metaBundle): ?int + { + $totalElements = $seoElement::sitemapElementsQuery($metaBundle)->count(); + + if ($metaBundle->metaSitemapVars->sitemapLimit && ($totalElements > $metaBundle->metaSitemapVars->sitemapLimit)) { + $totalElements = $metaBundle->metaSitemapVars->sitemapLimit; + } + + return $totalElements; + } /** * Combine any per-entry type field settings from $element with the passed in @@ -584,8 +601,4 @@ protected static function assetFilesSitemapLink(Asset $asset, MetaBundle $metaBu } } } - - protected static function getElementListSitemap(array $elements) - { - } } diff --git a/src/models/SitemapIndexTemplate.php b/src/models/SitemapIndexTemplate.php index 839d0bdf9..e28f66611 100644 --- a/src/models/SitemapIndexTemplate.php +++ b/src/models/SitemapIndexTemplate.php @@ -20,6 +20,7 @@ use nystudio107\seomatic\events\RegisterSitemapsEvent; use nystudio107\seomatic\events\RegisterSitemapUrlsEvent; use nystudio107\seomatic\helpers\MetaValue as MetaValueHelper; +use nystudio107\seomatic\helpers\Sitemap; use nystudio107\seomatic\Seomatic; use yii\base\Event; use yii\caching\TagDependency; @@ -191,11 +192,7 @@ public function render(array $params = []): string $metaBundle->metaSitemapVars->sitemapLimit = null; } - $totalElements = $seoElement::sitemapElementsQuery($metaBundle)->count(); - - if ($metaBundle->metaSitemapVars->sitemapLimit && ($totalElements > $metaBundle->metaSitemapVars->sitemapLimit)) { - $totalElements = $metaBundle->metaSitemapVars->sitemapLimit; - } + $totalElements = Sitemap::getTotalElementsInSitemap($seoElement, $metaBundle); $pageSize = $metaBundle->metaSitemapVars->sitemapPageSize; $pageCount = (!empty($pageSize) && $pageSize > 0) ? ceil($totalElements / $pageSize) : 1; @@ -221,7 +218,7 @@ public function render(array $params = []): string $lines[] = $metaBundle->sourceDateUpdated->format(DateTime::W3C); $lines[] = ''; } - + $lines[] = ''; } } diff --git a/src/models/SitemapTemplate.php b/src/models/SitemapTemplate.php index d2a542f1c..a0c9ea704 100644 --- a/src/models/SitemapTemplate.php +++ b/src/models/SitemapTemplate.php @@ -171,6 +171,7 @@ public function render(array $params = []): string $dependency = new TagDependency([ 'tags' => [ self::GLOBAL_SITEMAP_CACHE_TAG, + self::SITEMAP_CACHE_TAG . $handle . $siteId, self::SITEMAP_CACHE_TAG . $handle . $siteId . $pageCacheSuffix, ], ]); diff --git a/src/seoelements/SeoCampaign.php b/src/seoelements/SeoCampaign.php index c0174ca23..1b232628e 100644 --- a/src/seoelements/SeoCampaign.php +++ b/src/seoelements/SeoCampaign.php @@ -446,7 +446,6 @@ public static function createContentMetaBundle(Model $sourceModel) /** @var Site $site */ foreach ($sites as $site) { $seoElement = self::class; - /** @var SeoElementInterface $seoElement */ Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id); } } diff --git a/src/seoelements/SeoCategory.php b/src/seoelements/SeoCategory.php index 86d3d112f..7de33ce0d 100644 --- a/src/seoelements/SeoCategory.php +++ b/src/seoelements/SeoCategory.php @@ -442,7 +442,6 @@ public static function createContentMetaBundle(Model $sourceModel) /** @var Site $site */ foreach ($sites as $site) { $seoElement = self::class; - /** @var SeoElementInterface $seoElement */ Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id); } } diff --git a/src/seoelements/SeoDigitalProduct.php b/src/seoelements/SeoDigitalProduct.php index a58bec0f4..eee79b095 100644 --- a/src/seoelements/SeoDigitalProduct.php +++ b/src/seoelements/SeoDigitalProduct.php @@ -448,7 +448,6 @@ public static function createContentMetaBundle(Model $sourceModel) /** @var Site $site */ foreach ($sites as $site) { $seoElement = self::class; - /** @var SeoElementInterface $seoElement */ Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id); } } diff --git a/src/seoelements/SeoEntry.php b/src/seoelements/SeoEntry.php index b39e43110..4b65882d9 100644 --- a/src/seoelements/SeoEntry.php +++ b/src/seoelements/SeoEntry.php @@ -468,7 +468,6 @@ public static function createContentMetaBundle(Model $sourceModel) /** @var Site $site */ foreach ($sites as $site) { $seoElement = self::class; - /** @var SeoElementInterface $seoElement */ Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id); } } diff --git a/src/seoelements/SeoEvent.php b/src/seoelements/SeoEvent.php index 21e0660be..6290f931a 100644 --- a/src/seoelements/SeoEvent.php +++ b/src/seoelements/SeoEvent.php @@ -217,7 +217,6 @@ public static function createContentMetaBundle(Model $sourceModel) /** @var Site $site */ foreach ($sites as $site) { $seoElement = self::class; - /** @var SeoElementInterface $seoElement */ Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id); } } diff --git a/src/seoelements/SeoProduct.php b/src/seoelements/SeoProduct.php index dfa0fafd3..c7926fed2 100644 --- a/src/seoelements/SeoProduct.php +++ b/src/seoelements/SeoProduct.php @@ -482,7 +482,6 @@ public static function createContentMetaBundle(Model $sourceModel) /** @var Site $site */ foreach ($sites as $site) { $seoElement = self::class; - /** @var SeoElementInterface $seoElement */ Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id); } } diff --git a/src/seoelements/SeoShopifyProduct.php b/src/seoelements/SeoShopifyProduct.php index f879b7501..4aa162c3c 100644 --- a/src/seoelements/SeoShopifyProduct.php +++ b/src/seoelements/SeoShopifyProduct.php @@ -361,7 +361,6 @@ public static function createContentMetaBundle(?Model $sourceModel) /** @var Site $site */ foreach ($sites as $site) { $seoElement = self::class; - /** @var SeoElementInterface $seoElement */ Seomatic::$plugin->metaBundles->createMetaBundleFromSeoElement($seoElement, $sourceModel, $site->id); } } diff --git a/src/services/MetaBundles.php b/src/services/MetaBundles.php index 8144654a8..beaa940a9 100644 --- a/src/services/MetaBundles.php +++ b/src/services/MetaBundles.php @@ -336,7 +336,7 @@ public function updateMetaBundle(MetaBundle $metaBundle, int $siteId) } /** - * @param SeoElementInterface $seoElement + * @param class-string $seoElement * @param Model $sourceModel * @param int $sourceSiteId * @param MetaBundle|null $baseConfig diff --git a/src/services/SeoElements.php b/src/services/SeoElements.php index d4ccef9ee..2a19a97b6 100644 --- a/src/services/SeoElements.php +++ b/src/services/SeoElements.php @@ -81,18 +81,16 @@ class SeoElements extends Component /** * @param null|string $metaBundleType * - * @return SeoElementInterface|null + * @return class-string|null */ - public function getSeoElementByMetaBundleType($metaBundleType) + public function getSeoElementByMetaBundleType(?string $metaBundleType): ?string { if ($metaBundleType === null) { return null; } $seoElements = $this->getAllSeoElementTypes(); - /** @var SeoElementInterface $seoElement */ - $seoElement = $seoElements[$metaBundleType] ?? null; - return $seoElement; + return $seoElements[$metaBundleType] ?? null; } /** diff --git a/src/services/Sitemaps.php b/src/services/Sitemaps.php index e53d8dcd0..407424eaf 100644 --- a/src/services/Sitemaps.php +++ b/src/services/Sitemaps.php @@ -513,30 +513,12 @@ public function invalidateCaches() */ public function invalidateSitemapCache(string $handle, ?int $siteId, string $type, bool $invalidateCache = true) { - // Since we want a stale-while-revalidate pattern, only invalidate the cache if we're asked to - if ($invalidateCache) { - $cache = Craft::$app->getCache(); - TagDependency::invalidate($cache, SitemapTemplate::SITEMAP_CACHE_TAG . $handle . $siteId); - Craft::info( - 'Sitemap cache cleared: ' . $handle, - __METHOD__ - ); - } - $sites = Craft::$app->getSites(); - if ($siteId === null) { - $siteId = $sites->currentSite->id ?? 1; - } - $site = $sites->getSiteById($siteId); - $groupId = $site->groupId; - $sitemapTemplate = SitemapTemplate::create(); - $xml = $sitemapTemplate->render( - [ - 'groupId' => $groupId, - 'type' => $type, - 'handle' => $handle, - 'siteId' => $siteId, - 'throwException' => false, - ] + // Always just invalidate the sitemap cache now, since we're doing paginated sitemaps + $cache = Craft::$app->getCache(); + TagDependency::invalidate($cache, SitemapTemplate::SITEMAP_CACHE_TAG . $handle . $siteId); + Craft::info( + 'Sitemap cache cleared: ' . $handle, + __METHOD__ ); } diff --git a/src/templates/settings/plugin/_includes/sitemaps.twig b/src/templates/settings/plugin/_includes/sitemaps.twig index c9a0f2c5d..222b671e1 100644 --- a/src/templates/settings/plugin/_includes/sitemaps.twig +++ b/src/templates/settings/plugin/_includes/sitemaps.twig @@ -15,8 +15,8 @@ }) }} {{ forms.lightswitchField({ - label: "Regenerate Sitemaps Automatically"|t("seomatic"), - instructions: "Controls whether sitemaps will automatically be regenerated when entries are saved."|t("seomatic"), + label: "Invalidate Sitemap Caches Automatically"|t("seomatic"), + instructions: "Controls whether sitemap caches will automatically be invalidated when entries are saved."|t("seomatic"), id: "regenerateSitemapsAutomatically", name: "regenerateSitemapsAutomatically", on: settings.regenerateSitemapsAutomatically,