├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── build.yml ├── LICENSE ├── UPGRADE-3.0.md ├── bin └── create_node_symlink.php ├── composer.json ├── docker-compose.yml ├── ecs.php ├── phpstan.neon.dist ├── psalm.xml └── src ├── Builder ├── BuilderInterface.php ├── SitemapBuilder.php ├── SitemapBuilderInterface.php ├── SitemapIndexBuilder.php └── SitemapIndexBuilderInterface.php ├── Command └── GenerateSitemapCommand.php ├── Controller ├── AbstractController.php ├── SitemapController.php └── SitemapIndexController.php ├── DependencyInjection ├── Compiler │ └── SitemapProviderPass.php ├── Configuration.php └── SitemapExtension.php ├── Exception ├── RouteExistsException.php └── SitemapUrlNotFoundException.php ├── Factory ├── AlternativeUrlFactory.php ├── AlternativeUrlFactoryInterface.php ├── ImageFactory.php ├── ImageFactoryInterface.php ├── IndexUrlFactory.php ├── IndexUrlFactoryInterface.php ├── SitemapFactory.php ├── SitemapFactoryInterface.php ├── SitemapIndexFactory.php ├── SitemapIndexFactoryInterface.php ├── UrlFactory.php └── UrlFactoryInterface.php ├── Filesystem ├── Reader.php └── Writer.php ├── Generator ├── ProductImagesToSitemapImagesCollectionGenerator.php └── ProductImagesToSitemapImagesCollectionGeneratorInterface.php ├── Model ├── AlternativeUrl.php ├── AlternativeUrlInterface.php ├── ChangeFrequency.php ├── Image.php ├── ImageInterface.php ├── IndexUrl.php ├── IndexUrlInterface.php ├── Sitemap.php ├── SitemapIndex.php ├── SitemapInterface.php ├── Url.php └── UrlInterface.php ├── Provider ├── Data │ ├── DataProviderInterface.php │ ├── ProductDataProvider.php │ ├── ProductDataProviderInterface.php │ ├── TaxonDataProvider.php │ └── TaxonDataProviderInterface.php ├── IndexUrlProvider.php ├── IndexUrlProviderInterface.php ├── ProductUrlProvider.php ├── StaticUrlProvider.php ├── TaxonUrlProvider.php └── UrlProviderInterface.php ├── Renderer ├── RendererAdapterInterface.php ├── SitemapRenderer.php ├── SitemapRendererInterface.php └── TwigAdapter.php ├── Resources ├── config │ ├── config.yaml │ ├── routing.yml │ ├── services.xml │ └── services │ │ ├── providers │ │ ├── products.xml │ │ ├── static.xml │ │ └── taxons.xml │ │ └── sitemap.xml └── views │ ├── Macro │ ├── language.html.twig │ └── xml.html.twig │ ├── index.xml.twig │ └── show.xml.twig ├── Routing └── SitemapLoader.php └── SitemapPlugin.php /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @stefandoorn 2 | 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | allow: 10 | - dependency-type: direct 11 | - dependency-type: indirect 12 | ignore: 13 | - dependency-name: sylius-labs/coding-standard 14 | versions: 15 | - 4.0.2 16 | - dependency-name: symfony/intl 17 | versions: 18 | - 5.2.3 19 | - dependency-name: phpunit/phpunit 20 | versions: 21 | - 9.5.1 22 | - dependency-name: phpstan/phpstan-shim 23 | versions: 24 | - 0.12.0 25 | - dependency-name: lchrusciel/api-test-case 26 | versions: 27 | - 5.0.0 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'dependabot/**' 7 | pull_request: ~ 8 | release: 9 | types: [created] 10 | schedule: 11 | - 12 | cron: "0 1 * * 6" # Run at 1am every Saturday 13 | workflow_dispatch: ~ 14 | 15 | jobs: 16 | tests: 17 | runs-on: ubuntu-latest 18 | 19 | name: "Sylius ${{ matrix.sylius }}, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}" 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | php: ["8.2", "8.3"] 25 | symfony: ["^6.4", "^7.2"] 26 | sylius: ["~2.0.0", "~2.1.0"] 27 | node: ["18.x"] 28 | mysql: ["8.0"] 29 | 30 | env: 31 | APP_ENV: test 32 | DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?serverVersion=${{ matrix.mysql }}" 33 | 34 | steps: 35 | - 36 | uses: actions/checkout@v4 37 | 38 | - 39 | name: Setup PHP 40 | uses: shivammathur/setup-php@v2 41 | with: 42 | php-version: "${{ matrix.php }}" 43 | extensions: intl 44 | tools: symfony 45 | coverage: none 46 | 47 | - 48 | name: Shutdown default MySQL 49 | run: sudo service mysql stop 50 | 51 | - 52 | name: Setup MySQL 53 | uses: mirromutth/mysql-action@v1.1 54 | with: 55 | mysql version: "${{ matrix.mysql }}" 56 | mysql root password: "root" 57 | 58 | - 59 | name: Output PHP version for Symfony CLI 60 | run: php -v | head -n 1 | awk '{ print $2 }' > .php-version 61 | 62 | - 63 | name: Install certificates 64 | run: symfony server:ca:install 65 | 66 | - 67 | name: Run webserver 68 | run: (cd tests/Application && symfony server:start --port=8080 --dir=public --daemon) 69 | 70 | - 71 | name: Get Composer cache directory 72 | id: composer-cache 73 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 74 | 75 | - 76 | name: Cache Composer 77 | uses: actions/cache@v4 78 | with: 79 | path: ${{ steps.composer-cache.outputs.dir }} 80 | key: ${{ runner.os }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-sylius-${{ matrix.sylius }}-composer-${{ hashFiles('**/composer.json') }} 81 | restore-keys: | 82 | ${{ runner.os }}-php-${{ matrix.php }}-composer- 83 | 84 | - 85 | name: Allow Composer plugins 86 | run: | 87 | composer global config --no-plugins allow-plugins.symfony/flex true 88 | 89 | - 90 | name: Restrict Symfony version 91 | if: matrix.symfony != '' 92 | run: | 93 | composer global require --no-progress --no-scripts --no-plugins "symfony/flex:^1.10" 94 | composer config extra.symfony.require "${{ matrix.symfony }}" 95 | 96 | - 97 | name: Restrict Sylius version 98 | if: matrix.sylius != '' 99 | run: composer require "sylius/sylius:${{ matrix.sylius }}" --no-update --no-scripts --no-interaction 100 | 101 | - 102 | name: Install PHP dependencies 103 | run: composer install --no-interaction 104 | 105 | - 106 | name: Prepare test application database 107 | run: | 108 | (cd tests/Application && bin/console doctrine:database:create -vvv) 109 | (cd tests/Application && bin/console doctrine:schema:create -vvv) 110 | 111 | - 112 | name: Prepare test application cache 113 | run: (cd tests/Application && bin/console cache:warmup -vvv) 114 | 115 | - 116 | name: Load fixtures in test application 117 | run: (cd tests/Application && bin/console sylius:fixtures:load -n) 118 | 119 | - 120 | name: Validate composer.json 121 | run: composer validate --ansi --strict 122 | 123 | - 124 | name: Run security check 125 | run: symfony security:check 126 | 127 | - 128 | name: Check coding standard 129 | run: composer check-style 130 | 131 | - 132 | name: Run PHPStan 133 | run: composer analyse || true 134 | 135 | - 136 | name: Run tests 137 | run: composer test 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Stefan Doorn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /UPGRADE-3.0.md: -------------------------------------------------------------------------------- 1 | # Upgrade 2.x to 3.0 2 | 3 | ## Upgrade 4 | 5 | Upgrading might be as simple as running the following command: 6 | 7 | ```bash 8 | $ composer require stefandoorn/sitemap-plugin:^3.0 9 | ``` 10 | 11 | Keep reading to understand the main changes that happened as part of the 3.0 release. 12 | 13 | ## Main changes 14 | 15 | ### Sylius 16 | 17 | The plugin has been upgraded to work with Sylius ^2.0. 18 | 19 | Also, the testing structure has been updated as much possible to reflect `PluginSkeleton^2.0`. 20 | 21 | ### PHP 22 | 23 | Sylius 2.0 requires a minimum of PHP 8.2, and the plugin has been updated similarly. 24 | 25 | ### Filesystem 26 | 27 | Since Nov, 2022 Sylius uses Flysystem as it's default filesystem implementation. 28 | 29 | From Sylius 2.0, this driver has become the default. 30 | 31 | The plugin has been updated to use Flysystem. 32 | 33 | If you did make configuration changes, have a look at `src/Resources/config/config.yaml` for the new configuration. 34 | 35 | #### Breaking change 36 | 37 | `Filesystem/Reader::has` has been removed, as we can rely on Flysystem exceptions now. 38 | 39 | As a side benefit, this also saves an I/O operation. 40 | 41 | `AbstractController::$reader` has been made `private`. 42 | 43 | ### Data providers (potential breaking change) 44 | 45 | Both the `product` & `taxon` URL provider have been changed. The data fetching part of them has been extracted 46 | into separate services. 47 | 48 | This change should make it easier for you to adjust only the data fetching, and not adjust the actual URL provider as well. 49 | 50 | In case you have adjusted these providers, this might incur a breaking change for you. Please do review your implementation. 51 | 52 | -------------------------------------------------------------------------------- /bin/create_node_symlink.php: -------------------------------------------------------------------------------- 1 | `' . NODE_MODULES_FOLDER_NAME . '` already exists as a link or folder, keeping existing as may be intentional.' . PHP_EOL; 11 | exit(0); 12 | } else { 13 | echo '> Invalid symlink `' . NODE_MODULES_FOLDER_NAME . '` detected, recreating...' . PHP_EOL; 14 | if (!@unlink(NODE_MODULES_FOLDER_NAME)) { 15 | echo '> Could not delete file `' . NODE_MODULES_FOLDER_NAME . '`.' . PHP_EOL; 16 | exit(1); 17 | } 18 | } 19 | } 20 | 21 | /* try to create the symlink using PHP internals... */ 22 | $success = @symlink(PATH_TO_NODE_MODULES, NODE_MODULES_FOLDER_NAME); 23 | 24 | /* if case it has failed, but OS is Windows... */ 25 | if (!$success && strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 26 | /* ...then try a different approach which does not require elevated permissions and folder to exist */ 27 | echo '> This system is running Windows, creation of links requires elevated privileges,' . PHP_EOL; 28 | echo '> and target path to exist. Fallback to NTFS Junction:' . PHP_EOL; 29 | exec(sprintf('mklink /J %s %s 2> NUL', NODE_MODULES_FOLDER_NAME, PATH_TO_NODE_MODULES), $output, $returnCode); 30 | $success = $returnCode === 0; 31 | if (!$success) { 32 | echo '> Failed o create the required symlink' . PHP_EOL; 33 | exit(2); 34 | } 35 | } 36 | 37 | $path = @readlink(NODE_MODULES_FOLDER_NAME); 38 | /* check if link points to the intended directory */ 39 | if ($path && realpath($path) === realpath(PATH_TO_NODE_MODULES)) { 40 | echo '> Successfully created the symlink.' . PHP_EOL; 41 | exit(0); 42 | } 43 | 44 | echo '> Failed to create the symlink to `' . NODE_MODULES_FOLDER_NAME . '`.' . PHP_EOL; 45 | exit(3); 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stefandoorn/sitemap-plugin", 3 | "type": "sylius-plugin", 4 | "description": "Sitemap Plugin for Sylius", 5 | "keywords": [ 6 | "sylius", 7 | "sylius-plugin" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": "^8.2", 12 | "league/flysystem-bundle": "^3.0", 13 | "sylius/sylius": "^2.0" 14 | }, 15 | "require-dev": { 16 | "lchrusciel/api-test-case": "^5.1", 17 | "matthiasnoback/symfony-dependency-injection-test": "^6.0", 18 | "nyholm/psr7": "^1.8", 19 | "phpstan/extension-installer": "^1.0", 20 | "phpstan/phpstan": "^2.0", 21 | "phpstan/phpstan-doctrine": "^2.0", 22 | "phpstan/phpstan-strict-rules": "^2.0", 23 | "phpstan/phpstan-symfony": "^2.0", 24 | "phpstan/phpstan-webmozart-assert": "^2.0", 25 | "phpunit/phpunit": "^10.0", 26 | "sylius-labs/coding-standard": "^4.0", 27 | "symfony/browser-kit": "^6.4 || ^7.1", 28 | "symfony/debug-bundle": "^6.4 || ^7.1", 29 | "symfony/dotenv": "^6.4 || ^7.1", 30 | "symfony/intl": "^6.4 || ^7.1", 31 | "symfony/runtime": "^6.4 || ^7.0", 32 | "symfony/ux-icons": "^2.22", 33 | "symfony/web-profiler-bundle": "^6.4 || ^7.1", 34 | "symfony/webpack-encore-bundle": "^1.15 || ^2.2" 35 | }, 36 | "config": { 37 | "sort-packages": true, 38 | "bin-dir": "bin", 39 | "allow-plugins": { 40 | "dealerdirect/phpcodesniffer-composer-installer": true, 41 | "symfony/thanks": true, 42 | "phpstan/extension-installer": true, 43 | "symfony/runtime": true, 44 | "php-http/discovery": true 45 | } 46 | }, 47 | "extra": { 48 | "branch-alias": { 49 | "dev-master": "3.0-dev" 50 | } 51 | }, 52 | "autoload": { 53 | "psr-4": { 54 | "SitemapPlugin\\": "src/", 55 | "Tests\\SitemapPlugin\\": ["tests/", "tests/Application/src"] 56 | } 57 | }, 58 | "autoload-dev": { 59 | "classmap": [ 60 | "tests/Application/Kernel.php" 61 | ] 62 | }, 63 | "scripts": { 64 | "post-install-cmd": [ 65 | "php bin/create_node_symlink.php" 66 | ], 67 | "post-update-cmd": [ 68 | "php bin/create_node_symlink.php" 69 | ], 70 | "post-create-project-cmd": [ 71 | "php bin/create_node_symlink.php" 72 | ], 73 | "analyse": "bin/phpstan analyse", 74 | "check-style": "bin/ecs check --ansi src/ tests/", 75 | "fix-style": "ecs check --ansi src/ tests/ --fix", 76 | "phpunit": "bin/phpunit", 77 | "test": [ 78 | "@phpunit" 79 | ] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | db: 4 | image: mariadb:10.6 5 | environment: 6 | - MYSQL_DATABASE=sylius 7 | - MYSQL_ROOT_PASSWORD=root 8 | - MYSQL_USER=root 9 | - MYSQL_PASSWORD=root 10 | - MYSQL_ROOT_HOST=% 11 | command: --sql_mode="" 12 | ports: 13 | - "3306:3306" 14 | volumes: 15 | - ./docker/volumes/mysql:/var/lib/mysql 16 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | import('vendor/sylius-labs/coding-standard/ecs.php'); 10 | 11 | $config->paths([ 12 | __DIR__ . '/spec', 13 | __DIR__ . '/src', 14 | __DIR__ . '/tests', 15 | 'ecs.php', 16 | ]); 17 | 18 | $config->skip([ 19 | VisibilityRequiredFixer::class => ['*Spec.php'], 20 | 'tests/Application/*', 21 | ]); 22 | 23 | $config->ruleWithConfiguration( 24 | NativeFunctionInvocationFixer::class, 25 | ['include' => ['@all'], 'scope' => 'all', 'strict' => \true]); 26 | 27 | $config->ruleWithConfiguration( 28 | TrailingCommaInMultilineFixer::class, 29 | ['elements' => ['arguments', 'array_destructuring', 'arrays', 'match', 'parameters']]); 30 | }; 31 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 4 3 | 4 | reportUnmatchedIgnoredErrors: true 5 | 6 | paths: 7 | - src 8 | 9 | excludePaths: 10 | # Makes PHPStan crash 11 | - 'src/DependencyInjection/Configuration.php' 12 | 13 | # Test dependencies 14 | - 'tests/Application/app/**.php' 15 | - 'tests/Application/src/**.php' 16 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 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 | -------------------------------------------------------------------------------- /src/Builder/BuilderInterface.php: -------------------------------------------------------------------------------- 1 | providers[] = $provider; 24 | } 25 | 26 | public function getProviders(): iterable 27 | { 28 | return $this->providers; 29 | } 30 | 31 | public function build(UrlProviderInterface $provider, ChannelInterface $channel): SitemapInterface 32 | { 33 | $urls = []; 34 | 35 | $sitemap = $this->sitemapFactory->createNew(); 36 | $urls[] = [...$provider->generate($channel)]; 37 | 38 | $sitemap->setUrls(\array_merge(...$urls)); 39 | 40 | return $sitemap; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Builder/SitemapBuilderInterface.php: -------------------------------------------------------------------------------- 1 | providers[] = $provider; 27 | } 28 | 29 | public function addIndexProvider(IndexUrlProviderInterface $indexProvider): void 30 | { 31 | foreach ($this->providers as $provider) { 32 | $indexProvider->addProvider($provider); 33 | } 34 | 35 | $this->indexProviders[] = $indexProvider; 36 | } 37 | 38 | public function build(): SitemapInterface 39 | { 40 | $sitemap = $this->sitemapIndexFactory->createNew(); 41 | $urls = []; 42 | 43 | foreach ($this->indexProviders as $indexProvider) { 44 | $urls[] = [...$indexProvider->generate()]; 45 | } 46 | 47 | $sitemap->setUrls(\array_merge(...$urls)); 48 | 49 | return $sitemap; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Builder/SitemapIndexBuilderInterface.php: -------------------------------------------------------------------------------- 1 | addOption('channel', 'c', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Channel codes to generate. If none supplied, all channels will generated.'); 36 | } 37 | 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | foreach ($this->channels($input) as $channel) { 41 | $this->executeChannel($channel, $output); 42 | } 43 | 44 | return 0; 45 | } 46 | 47 | private function executeChannel(ChannelInterface $channel, OutputInterface $output): void 48 | { 49 | $output->writeln(\sprintf('Start generating sitemaps for channel "%s"', $channel->getName())); 50 | 51 | $this->router->getContext()->setHost($channel->getHostname() ?? 'localhost'); 52 | // TODO make sure providers are every time emptied (reset call or smth?) 53 | foreach ($this->sitemapBuilder->getProviders() as $provider) { 54 | $output->writeln(\sprintf('Start generating sitemap "%s" for channel "%s"', $provider->getName(), $channel->getCode())); 55 | 56 | $sitemap = $this->sitemapBuilder->build($provider, $channel); // TODO use provider instance, not the name 57 | $xml = $this->sitemapRenderer->render($sitemap); 58 | $path = $this->path($channel, \sprintf('%s.xml', $provider->getName())); 59 | 60 | $this->writer->write( 61 | $path, 62 | $xml, 63 | ); 64 | 65 | $output->writeln(\sprintf('Finished generating sitemap "%s" for channel "%s" at path "%s"', $provider->getName(), $channel->getCode(), $path)); 66 | } 67 | 68 | $output->writeln(\sprintf('Start generating sitemap index for channel "%s"', $channel->getCode())); 69 | 70 | $sitemap = $this->sitemapIndexBuilder->build(); 71 | $xml = $this->sitemapIndexRenderer->render($sitemap); 72 | $path = $this->path($channel, 'sitemap_index.xml'); 73 | 74 | $this->writer->write( 75 | $path, 76 | $xml, 77 | ); 78 | 79 | $output->writeln(\sprintf('Finished generating sitemap index for channel "%s" at path "%s"', $channel->getCode(), $path)); 80 | } 81 | 82 | private function path(ChannelInterface $channel, string $path): string 83 | { 84 | return \sprintf('%s/%s', $channel->getCode(), $path); 85 | } 86 | 87 | /** 88 | * @return iterable 89 | */ 90 | private function channels(InputInterface $input): iterable 91 | { 92 | /** @var iterable $channels */ 93 | $channels = self::hasChannelInput($input) 94 | ? $this->channelRepository->findBy(['code' => $input->getOption('channel'), 'enabled' => true]) 95 | : $this->channelRepository->findBy(['enabled' => true]); 96 | 97 | return $channels; 98 | } 99 | 100 | private static function hasChannelInput(InputInterface $input): bool 101 | { 102 | $inputValue = $input->getOption('channel'); 103 | 104 | if (\is_array($inputValue) && 0 === \count($inputValue)) { 105 | return false; 106 | } 107 | 108 | return null !== $inputValue; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | reader->getStream($path); 25 | 26 | while (!\feof($handle)) { 27 | echo \fread($handle, 8192); 28 | } 29 | } catch (UnableToReadFile | FilesystemException) { 30 | throw new NotFoundHttpException(\sprintf('File "%s" not found', $path)); 31 | } 32 | }); 33 | $response->headers->set('Content-Type', 'application/xml'); 34 | 35 | return $response; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Controller/SitemapController.php: -------------------------------------------------------------------------------- 1 | channelContext->getChannel()->getCode(), \sprintf('%s.xml', $name)); 23 | 24 | return $this->createResponse($path); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Controller/SitemapIndexController.php: -------------------------------------------------------------------------------- 1 | channelContext->getChannel()->getCode(), 'sitemap_index.xml'); 23 | 24 | return $this->createResponse($path); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/SitemapProviderPass.php: -------------------------------------------------------------------------------- 1 | has('sylius.sitemap_builder')) { 16 | return; 17 | } 18 | 19 | $builderDefinition = $container->findDefinition('sylius.sitemap_builder'); 20 | $builderIndexDefinition = $container->findDefinition('sylius.sitemap_index_builder'); 21 | $taggedProviders = $container->findTaggedServiceIds('sylius.sitemap_provider'); 22 | 23 | foreach ($taggedProviders as $id => $tags) { 24 | $builderIndexDefinition->addMethodCall('addProvider', [(new Reference($id))]); 25 | $builderDefinition->addMethodCall('addProvider', [(new Reference($id))]); 26 | } 27 | 28 | $taggedProvidersIndex = $container->findTaggedServiceIds('sylius.sitemap_index_provider'); 29 | foreach ($taggedProvidersIndex as $id => $tags) { 30 | $builderIndexDefinition->addMethodCall('addIndexProvider', [new Reference($id)]); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 16 | 17 | $rootNode 18 | ->children() 19 | ->arrayNode('providers') 20 | ->addDefaultsIfNotSet() 21 | ->children() 22 | ->booleanNode('products')->defaultTrue()->end() 23 | ->booleanNode('taxons')->defaultTrue()->end() 24 | ->booleanNode('static')->defaultTrue()->end() 25 | ->end() 26 | ->end() 27 | ->scalarNode('template') 28 | ->defaultValue('@SitemapPlugin/show.xml.twig') 29 | ->end() 30 | ->scalarNode('index_template') 31 | ->defaultValue('@SitemapPlugin/index.xml.twig') 32 | ->end() 33 | ->scalarNode('exclude_taxon_root') 34 | ->info('Often you don\'t want to include the root of your taxon tree as it has a generic name as \'products\'.') 35 | ->defaultTrue() 36 | ->end() 37 | ->scalarNode('hreflang') 38 | ->info('Whether to generate alternative URL versions for each locale. Defaults to true. Background: https://support.google.com/webmasters/answer/189077?hl=en.') 39 | ->defaultTrue() 40 | ->end() 41 | ->scalarNode('images') 42 | ->info('Add images to URL output in case the provider adds them. Defaults to true. Background: https://support.google.com/webmasters/answer/178636?hl=en') 43 | ->defaultTrue() 44 | ->end() 45 | ->arrayNode('static_routes') 46 | ->beforeNormalization()->castToArray()->end() 47 | ->info('In case you want to add static routes to your sitemap (e.g. homepage), configure them here. Defaults to homepage & contact page.') 48 | ->prototype('array') 49 | ->children() 50 | ->scalarNode('route') 51 | ->info('Name of route') 52 | ->isRequired() 53 | ->cannotBeEmpty() 54 | ->end() 55 | ->arrayNode('parameters') 56 | ->prototype('variable')->end() 57 | ->info('Add optional parameters to the route.') 58 | ->end() 59 | ->arrayNode('locales') 60 | ->prototype('scalar') 61 | ->info('Define which locales to add. If empty, it uses the default locales for channel context supplied') 62 | ->end() 63 | ->end() 64 | ->end() 65 | ->end() 66 | ->end() 67 | ; 68 | 69 | return $treeBuilder; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/DependencyInjection/SitemapExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($this->getConfiguration([], $container), $configs); 18 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 19 | $loader->load('services.xml'); 20 | 21 | $container->setParameter('sylius.sitemap_template', $config['template']); 22 | $container->setParameter('sylius.sitemap_index_template', $config['index_template']); 23 | $container->setParameter('sylius.sitemap_exclude_taxon_root', $config['exclude_taxon_root']); 24 | $container->setParameter('sylius.sitemap_hreflang', $config['hreflang']); 25 | $container->setParameter('sylius.sitemap_static', $config['static_routes']); 26 | $container->setParameter('sylius.sitemap_images', $config['images']); 27 | 28 | foreach ($config['providers'] as $provider => $setting) { 29 | $parameter = \sprintf('sylius.provider.%s', $provider); 30 | $container->setParameter($parameter, $setting); 31 | 32 | if ($setting === true) { 33 | $loader->load(\sprintf('services/providers/%s.xml', $provider)); 34 | } 35 | } 36 | } 37 | 38 | public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface 39 | { 40 | return new Configuration(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Exception/RouteExistsException.php: -------------------------------------------------------------------------------- 1 | getLocation()), 0, $previousException); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Factory/AlternativeUrlFactory.php: -------------------------------------------------------------------------------- 1 | filesystem->readStream($path); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Filesystem/Writer.php: -------------------------------------------------------------------------------- 1 | filesystem->write($path, $contents); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Generator/ProductImagesToSitemapImagesCollectionGenerator.php: -------------------------------------------------------------------------------- 1 | imagePreset = $imagePreset; 25 | } 26 | } 27 | 28 | public function generate(ProductInterface $product): Collection 29 | { 30 | $images = new ArrayCollection(); 31 | 32 | /** @var ProductImageInterface $image */ 33 | foreach ($product->getImages() as $image) { 34 | $path = $image->getPath(); 35 | 36 | if (null === $path) { 37 | continue; 38 | } 39 | 40 | $sitemapImage = $this->sitemapImageUrlFactory->createNew($this->imagineCacheManager->getBrowserPath($path, $this->imagePreset)); 41 | 42 | /** 43 | * @psalm-suppress InvalidArgument 44 | */ 45 | $images->add($sitemapImage); 46 | } 47 | 48 | return $images; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Generator/ProductImagesToSitemapImagesCollectionGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | setLocation($location); 14 | $this->setLocale($locale); 15 | } 16 | 17 | public function getLocation(): string 18 | { 19 | return $this->location; 20 | } 21 | 22 | public function setLocation(string $location): void 23 | { 24 | $this->location = $location; 25 | } 26 | 27 | public function getLocale(): string 28 | { 29 | return $this->locale; 30 | } 31 | 32 | public function setLocale(string $locale): void 33 | { 34 | $this->locale = $locale; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Model/AlternativeUrlInterface.php: -------------------------------------------------------------------------------- 1 | changeFrequency; 16 | } 17 | 18 | public static function always(): self 19 | { 20 | return new self('always'); 21 | } 22 | 23 | public static function hourly(): self 24 | { 25 | return new self('hourly'); 26 | } 27 | 28 | public static function daily(): self 29 | { 30 | return new self('daily'); 31 | } 32 | 33 | public static function weekly(): self 34 | { 35 | return new self('weekly'); 36 | } 37 | 38 | public static function monthly(): self 39 | { 40 | return new self('monthly'); 41 | } 42 | 43 | public static function yearly(): self 44 | { 45 | return new self('yearly'); 46 | } 47 | 48 | public static function never(): self 49 | { 50 | return new self('never'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Model/Image.php: -------------------------------------------------------------------------------- 1 | location; 24 | } 25 | 26 | public function setLocation(string $location): void 27 | { 28 | $this->location = $location; 29 | } 30 | 31 | public function getTitle(): ?string 32 | { 33 | return $this->title; 34 | } 35 | 36 | public function setTitle(string $title): void 37 | { 38 | $this->title = $title; 39 | } 40 | 41 | public function getCaption(): ?string 42 | { 43 | return $this->caption; 44 | } 45 | 46 | public function setCaption(string $caption): void 47 | { 48 | $this->caption = $caption; 49 | } 50 | 51 | public function getGeoLocation(): ?string 52 | { 53 | return $this->geoLocation; 54 | } 55 | 56 | public function setGeoLocation(string $geoLocation): void 57 | { 58 | $this->geoLocation = $geoLocation; 59 | } 60 | 61 | public function getLicense(): ?string 62 | { 63 | return $this->license; 64 | } 65 | 66 | public function setLicense(string $license): void 67 | { 68 | $this->license = $license; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Model/ImageInterface.php: -------------------------------------------------------------------------------- 1 | location; 20 | } 21 | 22 | public function setLocation(string $location): void 23 | { 24 | $this->location = $location; 25 | } 26 | 27 | public function getLastModification(): ?DateTimeInterface 28 | { 29 | return $this->lastModification; 30 | } 31 | 32 | public function setLastModification(?DateTimeInterface $lastModification): void 33 | { 34 | $this->lastModification = $lastModification; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Model/IndexUrlInterface.php: -------------------------------------------------------------------------------- 1 | urls = $urls; 21 | } 22 | 23 | public function getUrls(): iterable 24 | { 25 | return $this->urls; 26 | } 27 | 28 | public function addUrl(UrlInterface $url): void 29 | { 30 | $this->urls[] = $url; 31 | } 32 | 33 | public function removeUrl(UrlInterface $url): void 34 | { 35 | $key = \array_search($url, $this->urls, true); 36 | if (false === $key) { 37 | throw new SitemapUrlNotFoundException($url); 38 | } 39 | 40 | unset($this->urls[$key]); 41 | } 42 | 43 | public function setLocalization(string $localization): void 44 | { 45 | $this->localization = $localization; 46 | } 47 | 48 | public function getLocalization(): ?string 49 | { 50 | return $this->localization; 51 | } 52 | 53 | public function setLastModification(DateTimeInterface $lastModification): void 54 | { 55 | $this->lastModification = $lastModification; 56 | } 57 | 58 | public function getLastModification(): ?DateTimeInterface 59 | { 60 | return $this->lastModification; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Model/SitemapIndex.php: -------------------------------------------------------------------------------- 1 | urls = $urls; 21 | } 22 | 23 | public function getUrls(): iterable 24 | { 25 | return $this->urls; 26 | } 27 | 28 | public function addUrl(UrlInterface $url): void 29 | { 30 | $this->urls[] = $url; 31 | } 32 | 33 | public function removeUrl(UrlInterface $url): void 34 | { 35 | $key = \array_search($url, $this->urls, true); 36 | if (false === $key) { 37 | throw new SitemapUrlNotFoundException($url); 38 | } 39 | 40 | unset($this->urls[$key]); 41 | } 42 | 43 | public function setLocalization(string $localization): void 44 | { 45 | $this->localization = $localization; 46 | } 47 | 48 | public function getLocalization(): string 49 | { 50 | return $this->localization; 51 | } 52 | 53 | public function setLastModification(DateTimeInterface $lastModification): void 54 | { 55 | $this->lastModification = $lastModification; 56 | } 57 | 58 | public function getLastModification(): DateTimeInterface 59 | { 60 | return $this->lastModification; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Model/SitemapInterface.php: -------------------------------------------------------------------------------- 1 | alternatives = new ArrayCollection(); 28 | $this->images = new ArrayCollection(); 29 | } 30 | 31 | public function getLocation(): string 32 | { 33 | return $this->location; 34 | } 35 | 36 | public function setLocation(string $location): void 37 | { 38 | $this->location = $location; 39 | } 40 | 41 | public function getLastModification(): ?DateTimeInterface 42 | { 43 | return $this->lastModification; 44 | } 45 | 46 | public function setLastModification(DateTimeInterface $lastModification): void 47 | { 48 | $this->lastModification = $lastModification; 49 | } 50 | 51 | public function getChangeFrequency(): ?string 52 | { 53 | return $this->changeFrequency; 54 | } 55 | 56 | public function setChangeFrequency(ChangeFrequency $changeFrequency): void 57 | { 58 | $this->changeFrequency = (string) $changeFrequency; 59 | } 60 | 61 | public function getPriority(): ?float 62 | { 63 | return $this->priority; 64 | } 65 | 66 | public function setPriority(float $priority): void 67 | { 68 | if (0 > $priority || 1 < $priority) { 69 | throw new \InvalidArgumentException(\sprintf( 70 | 'The value %s is not supported by the option priority, it must be a number between 0.0 and 1.0.', 71 | $priority, 72 | )); 73 | } 74 | 75 | $this->priority = $priority; 76 | } 77 | 78 | public function getAlternatives(): Collection 79 | { 80 | return $this->alternatives; 81 | } 82 | 83 | public function setAlternatives(iterable $alternatives): void 84 | { 85 | $this->alternatives->clear(); 86 | 87 | foreach ($alternatives as $alternative) { 88 | $this->addAlternative($alternative); 89 | } 90 | } 91 | 92 | public function addAlternative(AlternativeUrlInterface $alternative): void 93 | { 94 | $this->alternatives->add($alternative); 95 | } 96 | 97 | public function hasAlternative(AlternativeUrlInterface $alternative): bool 98 | { 99 | return $this->alternatives->contains($alternative); 100 | } 101 | 102 | public function removeAlternative(AlternativeUrlInterface $alternative): void 103 | { 104 | if ($this->hasAlternative($alternative)) { 105 | $this->alternatives->removeElement($alternative); 106 | } 107 | } 108 | 109 | public function hasAlternatives(): bool 110 | { 111 | return !$this->alternatives->isEmpty(); 112 | } 113 | 114 | public function getImages(): Collection 115 | { 116 | return $this->images; 117 | } 118 | 119 | public function setImages(iterable $images): void 120 | { 121 | $this->images->clear(); 122 | 123 | foreach ($images as $image) { 124 | $this->addImage($image); 125 | } 126 | } 127 | 128 | public function addImage(ImageInterface $image): void 129 | { 130 | $this->images->add($image); 131 | } 132 | 133 | public function hasImage(ImageInterface $image): bool 134 | { 135 | return $this->images->contains($image); 136 | } 137 | 138 | public function removeImage(ImageInterface $image): void 139 | { 140 | if ($this->hasImage($image)) { 141 | $this->images->removeElement($image); 142 | } 143 | } 144 | 145 | public function hasImages(): bool 146 | { 147 | return !$this->images->isEmpty(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Model/UrlInterface.php: -------------------------------------------------------------------------------- 1 | repository->createQueryBuilder('o') 24 | ->addSelect('translation') 25 | ->innerJoin('o.translations', 'translation') 26 | ->andWhere(':channel MEMBER OF o.channels') 27 | ->andWhere('o.enabled = :enabled') 28 | ->setParameter('channel', $channel) 29 | ->setParameter('enabled', true) 30 | ->getQuery() 31 | ->getResult() 32 | ; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Provider/Data/ProductDataProviderInterface.php: -------------------------------------------------------------------------------- 1 | repository->findBy(['enabled' => true]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Provider/Data/TaxonDataProviderInterface.php: -------------------------------------------------------------------------------- 1 | providers[] = $provider; 24 | } 25 | 26 | public function generate(): iterable 27 | { 28 | $urls = []; 29 | foreach ($this->providers as $provider) { 30 | $location = $this->router->generate('sylius_sitemap_' . $provider->getName()); 31 | $urls[] = $this->sitemapIndexUrlFactory->createNew($location); 32 | } 33 | 34 | return $urls; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Provider/IndexUrlProviderInterface.php: -------------------------------------------------------------------------------- 1 | */ 27 | private array $channelLocaleCodes; 28 | 29 | public function __construct( 30 | private readonly ProductDataProviderInterface $dataProvider, 31 | private readonly RouterInterface $router, 32 | private readonly UrlFactoryInterface $urlFactory, 33 | private readonly AlternativeUrlFactoryInterface $urlAlternativeFactory, 34 | private readonly LocaleContextInterface $localeContext, 35 | private readonly ProductImagesToSitemapImagesCollectionGeneratorInterface $productToImageSitemapArrayGenerator, 36 | ) { 37 | } 38 | 39 | public function getName(): string 40 | { 41 | return 'products'; 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function generate(ChannelInterface $channel): iterable 48 | { 49 | $this->channel = $channel; 50 | $this->channelLocaleCodes = []; 51 | 52 | $urls = []; 53 | foreach ($this->dataProvider->get($channel) as $product) { 54 | $urls[] = $this->createProductUrl($product); 55 | } 56 | 57 | return $urls; 58 | } 59 | 60 | private function getTranslations(ProductInterface $product): Collection 61 | { 62 | return $product->getTranslations()->filter(function (TranslationInterface $translation): bool { 63 | return $this->localeInLocaleCodes($translation); 64 | }); 65 | } 66 | 67 | private function localeInLocaleCodes(TranslationInterface $translation): bool 68 | { 69 | return \in_array($translation->getLocale(), $this->getLocaleCodes(), true); 70 | } 71 | 72 | private function getLocaleCodes(): array 73 | { 74 | if ($this->channelLocaleCodes === []) { 75 | $this->channelLocaleCodes = $this->channel->getLocales()->map(function (LocaleInterface $locale): ?string { 76 | return $locale->getCode(); 77 | })->toArray(); 78 | } 79 | 80 | return $this->channelLocaleCodes; 81 | } 82 | 83 | private function createProductUrl(ProductInterface $product): UrlInterface 84 | { 85 | $productUrl = $this->urlFactory->createNew(''); // todo bypassing this new constructor right now 86 | $productUrl->setChangeFrequency(ChangeFrequency::always()); 87 | $productUrl->setPriority(0.5); 88 | $updatedAt = $product->getUpdatedAt(); 89 | if ($updatedAt !== null) { 90 | $productUrl->setLastModification($updatedAt); 91 | } 92 | $productUrl->setImages($this->productToImageSitemapArrayGenerator->generate($product)); 93 | 94 | /** @var ProductTranslationInterface $translation */ 95 | foreach ($this->getTranslations($product) as $translation) { 96 | $locale = $translation->getLocale(); 97 | 98 | if ($locale === null) { 99 | continue; 100 | } 101 | 102 | if (!$this->localeInLocaleCodes($translation)) { 103 | continue; 104 | } 105 | 106 | $location = $this->router->generate('sylius_shop_product_show', [ 107 | 'slug' => $translation->getSlug(), 108 | '_locale' => $translation->getLocale(), 109 | ]); 110 | 111 | if ($locale === $this->localeContext->getLocaleCode()) { 112 | $productUrl->setLocation($location); 113 | 114 | continue; 115 | } 116 | 117 | $productUrl->addAlternative($this->urlAlternativeFactory->createNew($location, $locale)); 118 | } 119 | 120 | return $productUrl; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Provider/StaticUrlProvider.php: -------------------------------------------------------------------------------- 1 | */ 22 | private readonly array $routes, 23 | ) { 24 | } 25 | 26 | public function getName(): string 27 | { 28 | return 'static'; 29 | } 30 | 31 | public function generate(ChannelInterface $channel): iterable 32 | { 33 | $this->channel = $channel; 34 | $urls = []; 35 | 36 | if (0 === \count($this->routes)) { 37 | return $urls; 38 | } 39 | 40 | foreach ($this->transformAndYieldRoutes() as $route) { 41 | $location = $this->router->generate($route['route'], $route['parameters']); 42 | 43 | $staticUrl = $this->sitemapUrlFactory->createNew($location); 44 | $staticUrl->setChangeFrequency(ChangeFrequency::weekly()); 45 | $staticUrl->setPriority(0.3); 46 | 47 | foreach ($route['locales'] as $alternativeLocaleCode) { 48 | $route['parameters']['_locale'] = $alternativeLocaleCode; 49 | $alternativeLocation = $this->router->generate($route['route'], $route['parameters']); 50 | $staticUrl->addAlternative($this->urlAlternativeFactory->createNew($alternativeLocation, $alternativeLocaleCode)); 51 | } 52 | 53 | $urls[] = $staticUrl; 54 | } 55 | 56 | return $urls; 57 | } 58 | 59 | private function transformAndYieldRoutes(): \Generator 60 | { 61 | foreach ($this->routes as $route) { 62 | yield $this->transformRoute($route); 63 | } 64 | } 65 | 66 | private function transformRoute(array $route): array 67 | { 68 | // Add default locale to route if not set 69 | $route = $this->addDefaultRoute($route); 70 | 71 | // Populate locales array by other enabled locales for current channel if no locales are specified 72 | if (!isset($route['locales']) || 0 === \count($route['locales'])) { 73 | $route['locales'] = $this->getAlternativeLocales(); 74 | } 75 | 76 | // Remove the locale that is on the main route from the alternatives to prevent duplicates 77 | $route = $this->excludeMainRouteLocaleFromAlternativeLocales($route); 78 | 79 | return $route; 80 | } 81 | 82 | private function addDefaultRoute(array $route): array 83 | { 84 | if (isset($route['parameters']['_locale'])) { 85 | return $route; 86 | } 87 | 88 | $defaultLocale = $this->channel->getDefaultLocale(); 89 | 90 | if (null !== $defaultLocale) { 91 | $route['parameters']['_locale'] = $defaultLocale->getCode(); 92 | } 93 | 94 | return $route; 95 | } 96 | 97 | private function excludeMainRouteLocaleFromAlternativeLocales(array $route): array 98 | { 99 | $locales = $route['locales']; 100 | $locale = $route['parameters']['_locale']; 101 | 102 | $key = \array_search($locale, $locales, true); 103 | 104 | if ($key !== false) { 105 | unset($route['locales'][$key]); 106 | } 107 | 108 | return $route; 109 | } 110 | 111 | /** 112 | * @return array 113 | */ 114 | private function getAlternativeLocales(): array 115 | { 116 | $locales = []; 117 | 118 | foreach ($this->channel->getLocales() as $locale) { 119 | if ($locale === $this->channel->getDefaultLocale()) { 120 | continue; 121 | } 122 | 123 | $locales[] = $locale->getCode(); 124 | } 125 | 126 | return $locales; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Provider/TaxonUrlProvider.php: -------------------------------------------------------------------------------- 1 | dataProvider->get($channel) as $taxon) { 39 | /** @var TaxonInterface $taxon */ 40 | if ($this->excludeTaxonRoot && $taxon->isRoot()) { 41 | continue; 42 | } 43 | 44 | $taxonUrl = $this->sitemapUrlFactory->createNew(''); // todo bypassing this new constructor right now 45 | $taxonUrl->setChangeFrequency(ChangeFrequency::always()); 46 | $taxonUrl->setPriority(0.5); 47 | 48 | /** @var TaxonTranslationInterface $translation */ 49 | foreach ($taxon->getTranslations() as $translation) { 50 | $location = $this->router->generate('sylius_shop_product_index', [ 51 | 'slug' => $translation->getSlug(), 52 | '_locale' => $translation->getLocale(), 53 | ]); 54 | 55 | if ($translation->getLocale() === $this->localeContext->getLocaleCode()) { 56 | $taxonUrl->setLocation($location); 57 | 58 | continue; 59 | } 60 | 61 | $locale = $translation->getLocale(); 62 | if (null !== $locale) { 63 | $taxonUrl->addAlternative($this->urlAlternativeFactory->createNew($location, $locale)); 64 | } 65 | } 66 | 67 | $urls[] = $taxonUrl; 68 | } 69 | 70 | return $urls; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Provider/UrlProviderInterface.php: -------------------------------------------------------------------------------- 1 | adapter->render($sitemap); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Renderer/SitemapRendererInterface.php: -------------------------------------------------------------------------------- 1 | twig->render($this->template, [ 19 | 'url_set' => $sitemap->getUrls(), 20 | 'hreflang' => $this->hreflang, 21 | 'images' => $this->images, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Resources/config/config.yaml: -------------------------------------------------------------------------------- 1 | sitemap: 2 | static_routes: 3 | - { route: sylius_shop_homepage } 4 | - { route: sylius_shop_contact_request } 5 | 6 | parameters: 7 | sylius.sitemap.path: "%kernel.project_dir%/var/sitemap" 8 | 9 | flysystem: 10 | storages: 11 | flysystem.storage.sylius_sitemap: 12 | adapter: 'local' 13 | options: 14 | directory: "%sylius.sitemap.path%" 15 | lazy_root_creation: true 16 | -------------------------------------------------------------------------------- /src/Resources/config/routing.yml: -------------------------------------------------------------------------------- 1 | # Index file holding references to all generated sitemaps (per provider) 2 | sylius_sitemap_index: 3 | path: /sitemap_index.xml 4 | methods: [GET] 5 | defaults: 6 | _controller: sylius.controller.sitemap_index::showAction 7 | 8 | # Redirect always to the index, as this is the preferred way 9 | sylius_sitemap_no_index: 10 | path: /sitemap.xml 11 | defaults: 12 | _controller: 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::redirectAction' 13 | route: sylius_sitemap_index 14 | permanent: true 15 | 16 | # Registering routes for each provider 17 | sylius_sitemap_providers: 18 | resource: . 19 | type: sitemap 20 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Resources/config/services/providers/products.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Resources/config/services/providers/static.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | %sylius.sitemap_static% 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Resources/config/services/providers/taxons.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | %sylius.sitemap_exclude_taxon_root% 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Resources/config/services/sitemap.xml: -------------------------------------------------------------------------------- 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 | %sylius.sitemap_template% 40 | %sylius.sitemap_hreflang% 41 | %sylius.sitemap_images% 42 | 43 | 44 | 45 | %sylius.sitemap_index_template% 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 | sylius_shop_product_original 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/Resources/views/Macro/language.html.twig: -------------------------------------------------------------------------------- 1 | {%- macro localeToCode(locale) -%} 2 | {{- locale|split('_')|first -}} 3 | {%- endmacro -%} 4 | -------------------------------------------------------------------------------- /src/Resources/views/Macro/xml.html.twig: -------------------------------------------------------------------------------- 1 | {%- macro last_modification(url) -%} 2 | {# @var url \SitemapPlugin\Model\SitemapUrlInterface #} 3 | {%- if url.lastModification is not same as(null) -%} 4 | {{ url.lastModification|date('c') }} 5 | {%- endif -%} 6 | {%- endmacro -%} 7 | 8 | {%- macro change_frequency(url) -%} 9 | {# @var url \SitemapPlugin\Model\SitemapUrlInterface #} 10 | {%- if url.changeFrequency is not same as(null) -%} 11 | {{ url.changeFrequency }} 12 | {%- endif -%} 13 | {%- endmacro -%} 14 | 15 | {%- macro priority(url) -%} 16 | {%- if url.priority is not same as(null) -%} 17 | {{ url.priority }} 18 | {%- endif -%} 19 | {%- endmacro -%} 20 | 21 | {%- macro images(url) -%} 22 | {%- if url.getImages is not empty -%} 23 | {%- for image in url.getImages -%} 24 | 25 | {{ image.location }} 26 | {%- if image.title is not empty -%} 27 | {{ image.title }} 28 | {%- endif -%} 29 | {%- if image.caption is not empty -%} 30 | {{ image.caption }} 31 | {%- endif -%} 32 | {%- if image.geoLocation is not empty -%} 33 | {{ image.geoLocation }} 34 | {%- endif -%} 35 | {%- if image.license is not empty -%} 36 | {{ image.license }} 37 | {%- endif -%} 38 | 39 | {%- endfor -%} 40 | {%- endif -%} 41 | {%- endmacro -%} 42 | -------------------------------------------------------------------------------- /src/Resources/views/index.xml.twig: -------------------------------------------------------------------------------- 1 | {% import '@SitemapPlugin/Macro/xml.html.twig' as xml_helper %} 2 | {% apply spaceless %} 3 | 4 | 5 | {%- for url in url_set -%} 6 | 7 | {{ absolute_url(url.location) }} 8 | {{- xml_helper.last_modification(url) -}} 9 | 10 | {% endfor %} 11 | 12 | {% endapply %} 13 | -------------------------------------------------------------------------------- /src/Resources/views/show.xml.twig: -------------------------------------------------------------------------------- 1 | {% import '@SitemapPlugin/Macro/language.html.twig' as language_helper %} 2 | {% import '@SitemapPlugin/Macro/xml.html.twig' as xml_helper %} 3 | {% apply spaceless %} 4 | 5 | 6 | {%- for url in url_set -%} 7 | 8 | {{ absolute_url(url.location) }} 9 | {% if hreflang is not same as(false) and url.alternatives is not empty %} 10 | 11 | {% for alternative in url.alternatives %} 12 | 13 | {% endfor %} 14 | {% endif %} 15 | {{ xml_helper.last_modification(url) }} 16 | {{ xml_helper.change_frequency(url) }} 17 | {{ xml_helper.priority(url) }} 18 | {%- if images -%} 19 | {{ xml_helper.images(url) }} 20 | {%- endif -%} 21 | 22 | {% if hreflang is not same as(false) and url.alternatives is not empty %} 23 | {% for alternative in url.alternatives %} 24 | 25 | {{ absolute_url(alternative.location) }} 26 | 27 | {% for alternativeSub in url.alternatives %} 28 | 29 | {% endfor %} 30 | {{ xml_helper.last_modification(url) }} 31 | {{ xml_helper.change_frequency(url) }} 32 | {{ xml_helper.priority(url) }} 33 | {%- if images -%} 34 | {{ xml_helper.images(url) }} 35 | {%- endif -%} 36 | 37 | {% endfor %} 38 | {% endif %} 39 | {%- endfor -%} 40 | 41 | {% endapply %} 42 | -------------------------------------------------------------------------------- /src/Routing/SitemapLoader.php: -------------------------------------------------------------------------------- 1 | loaded) { 30 | return $routes; 31 | } 32 | 33 | foreach ($this->sitemapBuilder->getProviders() as $provider) { 34 | $name = 'sylius_sitemap_' . $provider->getName(); 35 | 36 | if (null !== $routes->get($name)) { 37 | throw new RouteExistsException($name); 38 | } 39 | 40 | $routes->add( 41 | $name, 42 | new Route( 43 | '/sitemap/' . $provider->getName() . '.xml', 44 | [ 45 | '_controller' => 'sylius.controller.sitemap::showAction', 46 | 'name' => $provider->getName(), 47 | ], 48 | [], 49 | [], 50 | '', 51 | [], 52 | ['GET'], 53 | ), 54 | ); 55 | } 56 | 57 | $this->loaded = true; 58 | 59 | return $routes; 60 | } 61 | 62 | public function supports($resource, $type = null): bool 63 | { 64 | return 'sitemap' === $type; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/SitemapPlugin.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new SitemapProviderPass()); 21 | } 22 | } 23 | --------------------------------------------------------------------------------