Solved: Shopware 6 sitemap generates URLs with http instead of https
The problem: unexpected core behavior
Based on this Shopware 6 official forum post, asking about a possible problem with sitemaps being generated with http instead https, I have checked Shopware 6 stores under my jurisdiction and indeed found out, that the problem is there. So I immediately started debugging. My starting point was the command, that takes care of the sitemap generation:
1 |
php bin/console sitemap:generate |
It is located in vendor/shopware/core/Content/Sitemap/Commands/SitemapGenerateCommand.php. After some digging in the classes and methods, that this command calls, I have found the problem in vendor/shopware/core/Content/Sitemap/Service/SitemapExporter.php. To be more specific, the method getHost of this class goes through the domains, that were associated to a sales channel (plus language) and picks the URL of the first one, that it encounters. And that is exactly the problem here. If you have both http and https versions of the domain, then Shopware can select an unwanted version as a base for your URLs.
Is this a bug? Maybe, maybe not.. But it could have certainly been handled better. The best option would be, if there was a setting in Administration, that would allow you to designate, which domain should be preffered in case there is more of them for a combination of sales channel and language. Alas, there is no such setting yet or I have not found it. To be honest, I think, that in most cases, people would prefer to use https over http. So the programmatic solution, that I will present here will be counting on that.
If some of the core functionality is not good enough or does not work, as we would like it to work, we have an option to override it from our custom plugin. And that is exactly what we are going to do. I will just assume, that we already have a plugin and describe just the parts, that are necessary for this solution.
The solution: class override
Step one – create an overiding class
First, we need to override the Shopware class, that contains the inconvenient code. So we will create a file, that will be named precisely the same, as the original and put it into a similar directory structure. In case of core class vendor/shopware/core/Content/Sitemap/Service/SitemapExporter.php the path to our override class within our plugin would be src/Core/Content/Sitemap/Service/SitemapExporter.php. My plugin is named ShopwareFixes (yes, I have already overridden some other stuff from the core 😀 ), so the result will look something like this:
I consider retaining the path a good practice for a better orientation in the code, but it probably is not a requirement. Now we copy the contents of the original file to the new one and do the necessary edits:
- change the namespace to reflect the location of the override
- add ‘use’ for the classes, that were omitted in the original class, because they were in the same namespace
- change the logic of the getHost method
This is how the override looks like after the edits:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
<?php declare(strict_types=1); namespace ShopwareFixes\Core\Content\Sitemap\Service; use League\Flysystem\FilesystemInterface; use Psr\Cache\CacheItemPoolInterface; use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface; use Shopware\Core\Content\Sitemap\Exception\AlreadyLockedException; use Shopware\Core\Content\Sitemap\Provider\UrlProviderInterface; use Shopware\Core\Content\Sitemap\Service\SitemapExporterInterface; use Shopware\Core\Content\Sitemap\Service\SitemapHandleFactoryInterface; use Shopware\Core\Content\Sitemap\Struct\SitemapGenerationResult; use Shopware\Core\System\SalesChannel\Aggregate\SalesChannelDomain\SalesChannelDomainCollection; use Shopware\Core\System\SalesChannel\SalesChannelContext; class SitemapExporter implements SitemapExporterInterface { /** * @var UrlProviderInterface[] */ private $urlProvider; /** * @var CacheItemPoolInterface */ private $cache; /** * @var int */ private $batchSize; /** * @var SeoUrlPlaceholderHandlerInterface */ private $seoUrlPlaceholderHandler; /** * @var FilesystemInterface */ private $filesystem; /** * @var SitemapHandleFactoryInterface */ private $sitemapHandleFactory; public function __construct( iterable $urlProvider, CacheItemPoolInterface $cache, int $batchSize, SeoUrlPlaceholderHandlerInterface $seoUrlPlaceholderHandler, FilesystemInterface $filesystem, SitemapHandleFactoryInterface $sitemapHandleFactory ) { $this->urlProvider = $urlProvider; $this->cache = $cache; $this->batchSize = $batchSize; $this->seoUrlPlaceholderHandler = $seoUrlPlaceholderHandler; $this->filesystem = $filesystem; $this->sitemapHandleFactory = $sitemapHandleFactory; } /** * {@inheritdoc} */ public function generate(SalesChannelContext $salesChannelContext, bool $force = false, ?string $lastProvider = null, ?int $offset = null): SitemapGenerationResult { $this->lock($salesChannelContext, $force); try { $host = $this->getHost($salesChannelContext); $sitemapHandle = $this->sitemapHandleFactory->create($this->filesystem, $salesChannelContext); foreach ($this->urlProvider as $urlProvider) { do { $result = $urlProvider->getUrls($salesChannelContext, $this->batchSize, $offset); foreach ($result->getUrls() as $url) { $url->setLoc($this->seoUrlPlaceholderHandler->replace($url->getLoc(), $host, $salesChannelContext)); } $sitemapHandle->write($result->getUrls()); $needRun = $result->getNextOffset() !== null; $offset = $result->getNextOffset(); } while ($needRun); } $sitemapHandle->finish(); } finally { $this->unlock($salesChannelContext); } return new SitemapGenerationResult( true, $lastProvider, null, $salesChannelContext->getSalesChannel()->getId(), $salesChannelContext->getSalesChannel()->getLanguageId() ); } private function lock(SalesChannelContext $salesChannelContext, bool $force): void { $key = $this->generateCacheKeyForSalesChannel($salesChannelContext); $item = $this->cache->getItem($key); if ($item->isHit() && !$force) { throw new AlreadyLockedException($salesChannelContext); } $item->set(true); $this->cache->save($item); } private function unlock(SalesChannelContext $salesChannelContext): void { $this->cache->deleteItem($this->generateCacheKeyForSalesChannel($salesChannelContext)); } private function generateCacheKeyForSalesChannel(SalesChannelContext $salesChannelContext): string { return sprintf('sitemap-exporter-running-%s-%s', $salesChannelContext->getSalesChannel()->getId(), $salesChannelContext->getSalesChannel()->getLanguageId()); } private function getHost(SalesChannelContext $salesChannelContext): string { $domains = $salesChannelContext->getSalesChannel()->getDomains(); $languageId = $salesChannelContext->getSalesChannel()->getLanguageId(); if ($domains instanceof SalesChannelDomainCollection) { foreach ($domains as $domain) { if ($domain->getLanguageId() === $languageId) { //get the URL and save it to the variable $url $url = $domain->getUrl(); //if this URL is https, return it immediately if (substr($url,0, 5) === 'https') { return $url; } } } //if we have URL at this point, it is clear, that no https was present and we can return this one if (isset($url)){ return $url; } } return ''; } } |
Step 2 – tell Shopware to use the override
Now we have to tell Shopware 6, that we want it to use our class instead of the original from the core. This is done in the services.xml file, located in the src/Resource/config. We need to add a new entry for our class (=service), where we specify, which class it overrides (=decorates in Symfony terminology). And as with any other services in Shopware 6, this one has to have its dependency injections specified as arguments.
Because the original class has quite a lot of dependencies, it is best to find the services.xml file, that contains the original class and copy them to our file.
The resulting file services.xml for our override looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="ShopwareFixes\Core\Content\Sitemap\Service\SitemapExporter" decorates="Shopware\Core\Content\Sitemap\Service\SitemapExporter" public="true"> <argument type="tagged" tag="shopware.sitemap_url_provider"/> <argument type="service" id="cache.system"/> <argument>%shopware.sitemap.batchsize%</argument> <argument type="service" id="Shopware\Core\Content\Seo\SeoUrlPlaceholderHandlerInterface"/> <argument type="service" id="shopware.filesystem.sitemap"/> <argument type="service" id="Shopware\Core\Content\Sitemap\Service\SitemapHandleFactoryInterface"/> <argument type="service" id="ShopwareFixes\Core\Content\Sitemap\Service\SitemapExporter.inner"/> </service> </services> </container> |
The last argument contains the injection of the original class, but in this case we do not need it.
And that is it, problem solved. If you now run the sitemap generation command again and you have a https domain in your sales channel, it will be used for URL generation for your sitemaps.