Skip to content

Commit

Permalink
feat: sitemap.xml (#183)
Browse files Browse the repository at this point in the history
* WIP

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml

* feat: sitemap.xml
  • Loading branch information
dkarlovi authored Dec 8, 2023
1 parent b6aa8fd commit 117fbf4
Show file tree
Hide file tree
Showing 20 changed files with 237 additions and 227 deletions.
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
"license": "MIT",
"require": {
"php": "^8.2",
"ext-zlib": "*",
"bentools/cartesian-product": "^1.4",
"league/commonmark": "^2.3",
"phpdocumentor/type-resolver": "^1.0",
"phpstan/phpdoc-parser": "^1.0",
"presta/sitemap-bundle": "^3.0",
"spatie/commonmark-highlighter": "^3.0",
"symfony/console": "^6.4 || ^7.0",
"symfony/expression-language": "^6.4 || ^7.0",
Expand Down
1 change: 1 addition & 0 deletions config/bundles.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Presta\SitemapBundle\PrestaSitemapBundle::class => ['all' => true],
];
27 changes: 19 additions & 8 deletions psalm.baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,11 @@
<code>__construct</code>
</PossiblyUnusedMethod>
</file>
<file src="src/DatabaseProvider.php">
<PossiblyUnusedMethod>
<code>__construct</code>
</PossiblyUnusedMethod>
</file>
<file src="src/Decoder/CachingFileDecoder.php">
<UnusedClass>
<code>CachingFileDecoder</code>
Expand All @@ -536,6 +541,11 @@
<code>YamlFileDecoder</code>
</UnusedClass>
</file>
<file src="src/Generator.php">
<PossiblyUnusedMethod>
<code>__construct</code>
</PossiblyUnusedMethod>
</file>
<file src="src/Permutator.php">
<MixedArgument>
<code>$expression</code>
Expand Down Expand Up @@ -570,6 +580,9 @@
<code><![CDATA[$spec['defaults']]]></code>
<code><![CDATA[$spec['options']]]></code>
</PossiblyUndefinedStringArrayOffset>
<PossiblyUnusedMethod>
<code>__construct</code>
</PossiblyUnusedMethod>
</file>
<file src="src/Storage/DenormalizingStorage.php">
<MissingTemplateParam>
Expand Down Expand Up @@ -635,6 +648,11 @@
<code>array</code>
</PossiblyUnusedReturnValue>
</file>
<file src="tests/functional/site/src/Bridge/Symfony/EventSubscriber.php">
<UnusedClass>
<code>EventSubscriber</code>
</UnusedClass>
</file>
<file src="tests/functional/site/src/Controller/TestController.php">
<UnusedClass>
<code>TestController</code>
Expand All @@ -643,6 +661,7 @@
<file src="tests/functional/site/src/Model/Article.php">
<MissingConstructor>
<code>$body</code>
<code>$image</code>
<code>$publishedAt</code>
<code>$slug</code>
<code>$title</code>
Expand Down Expand Up @@ -705,14 +724,6 @@
<code>$slug</code>
</PossiblyUnusedProperty>
</file>
<file src="tests/unit/GeneratorTest.php">
<InternalMethod>
<code>numberOfInvocations</code>
</InternalMethod>
<MixedAssignment>
<code>$params</code>
</MixedAssignment>
</file>
<file src="web/index.php">
<MixedArgument>
<code><![CDATA[$GLOBALS['YASSG_BASEDIR'] ?? throw new LogicException('YASSG base dir not found')]]></code>
Expand Down
39 changes: 39 additions & 0 deletions src/Bridge/PrestaSitemap/Urlset.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sigwin Yassg project.
*
* (c) sigwin.hr
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sigwin\YASSG\Bridge\PrestaSitemap;

use Presta\SitemapBundle\Sitemap\Url\Url;
use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;

final class Urlset extends \Presta\SitemapBundle\Sitemap\Urlset
{
public function __construct(string $loc)
{
parent::__construct($loc);

$this->lastmod = new \DateTimeImmutable('1970-01-01 00:00:00');
}

public function addUrl(Url $url): void
{
parent::addUrl($url);

if ($url instanceof UrlConcrete) {
$lastModification = $url->getLastmod();
if ($lastModification !== null && $lastModification->getTimestamp() > $this->lastmod->getTimestamp()) {
$this->lastmod = $lastModification;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@

namespace Sigwin\YASSG\Bridge\Symfony\Routing\Generator;

use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;

Expand All @@ -24,7 +23,7 @@ public function __construct(private readonly UrlGeneratorInterface $urlGenerator
public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string
{
if (! isset($this->routes[$name])) {
throw new RouteNotFoundException();
return $this->urlGenerator->generate($name, $parameters, $referenceType);
}

$this->stripParameters($this->stripParameters[$name] ?? [], $parameters);
Expand Down
85 changes: 82 additions & 3 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@

namespace Sigwin\YASSG;

use Presta\SitemapBundle\Sitemap\Sitemapindex;
use Presta\SitemapBundle\Sitemap\Url\UrlConcrete;
use Sigwin\YASSG\Bridge\PrestaSitemap\Urlset;
use Sigwin\YASSG\Bridge\Symfony\Routing\Request;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
Expand All @@ -32,19 +36,76 @@ public function generate(callable $callable): void

$indexFile = (bool) ($requestContext->getParameter('index-file') ?? false);

$deflate = true;
$index = new Sitemapindex();
$offset = 0;
$urlSet = null;
$urlSetAdded = true;
foreach ($this->permutator->permute() as $location) {
if ($urlSet !== null) {
if ($this->generateSitemapPath($deflate, $location->getRoute()->getName(), $offset) !== $urlSet->getLoc()) {
$this->dumpSitemap($urlSet, $deflate);
$index->addSitemap($urlSet);
$urlSet = null;
$urlSetAdded = false;
} elseif ($urlSet->isFull()) {
$this->dumpSitemap($urlSet, $deflate);
$index->addSitemap($urlSet);
$urlSet = null;
$urlSetAdded = false;
++$offset;
}
}
if ($urlSet === null) {
$urlSet = new Urlset($this->generateSitemapPath($deflate, $location->getRoute()->getName(), $offset));
$urlSetAdded = false;
$offset = 0;
}

$route = $location->getRoute();
$url = $this->urlGenerator->generate($route->getName(), $route->getParameters() + ($indexFile ? ['_filename' => 'index.html'] : []), UrlGeneratorInterface::ABSOLUTE_URL);
$request = Request::create(rtrim($url, '/'))->withBaseUrl($requestContext->getBaseUrl());
$request = $this->createRequest($url);
if (($buildHeaders = $location->getBuildOptions()->getRequestHeaders()) !== null) {
$request->headers->add($buildHeaders);
}

$this->dumpFile($callable, $request);
$response = $this->dumpRequest($callable, $request);
$urlSet->addUrl(new UrlConcrete($url, new \DateTimeImmutable($response->headers->get('Last-Modified', 'now'))));
}
if ($urlSet !== null) {
$this->dumpSitemap($urlSet, $deflate);
if ($urlSetAdded === false) {
$index->addSitemap($urlSet);
}
}
$this->dumpSitemap($index, $deflate);

// dump static files
$this->dumpRequest($callable, $this->createRequest($this->urlGenerator->generate('error404', [], UrlGeneratorInterface::ABSOLUTE_URL)), 404);
}

private function createRequest(string $path): Request
{
return Request::create(rtrim($path, '/'))->withBaseUrl($this->urlGenerator->getContext()->getBaseUrl());
}

private function generateSitemapPath(bool $deflate, ?string $name = null, ?int $offset = null): string
{
if ($name === null) {
return $this->generateUrl('/sitemap.xml'.($deflate ? '.gz' : ''));
}

return $this->generateUrl(sprintf('/sitemap-%1$s-%2$d.xml'.($deflate ? '.gz' : ''), $name, $offset ?? throw new \LogicException('Offset must be set when name is set')));
}

private function dumpFile(callable $callable, Request $request, int $expectedStatusCode = 200): void
private function generateUrl(string $path): string
{
$context = $this->urlGenerator->getContext();

return sprintf('%1$s://%2$s%3$s%4$s', $context->getScheme(), $context->getHost(), $context->getBaseUrl(), $path);
}

private function dumpRequest(callable $callable, Request $request, int $expectedStatusCode = 200): Response
{
try {
$response = $this->kernel->handle($request);
Expand All @@ -71,5 +132,23 @@ private function dumpFile(callable $callable, Request $request, int $expectedSta
$this->filesystem->dumpFile($path, $body);

$callable($request, $response, $path);

return $response;
}

private function dumpSitemap(Sitemapindex|Urlset $sitemap, bool $deflate): void
{
if ($sitemap->count() === 0) {
return;
}

$path = $this->generateSitemapPath($deflate);
if ($sitemap instanceof Urlset) {
$path = $sitemap->getLoc();
}

/** @var string $content */
$content = $deflate ? gzdeflate($sitemap->toXml()) : $sitemap->toXml();
$this->filesystem->dumpFile($this->buildDir.str_replace($this->generateUrl(''), '', $path), $content);
}
}
24 changes: 24 additions & 0 deletions tests/functional/init/fixtures/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex,nofollow,noarchive">
<title>An Error Occurred: Not Found</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>❌</text></svg>">
<style>body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; }
.container { margin: 30px; max-width: 600px; }
h1 { color: #dc3545; font-size: 24px; }
h2 { font-size: 18px; }</style>
</head>
<body>
<div class="container">
<h1>Oops! An Error Occurred</h1>
<h2>The server returned a "404 Not Found".</h2>

<p>
Something is broken. Please let us know what you were doing when this error occurred.
We will fix it as soon as possible. Sorry for any inconvenience caused.
</p>
</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
include ../default.mk

self/test:
cp public/sitemap* fixtures/
sh -c "${PHPQA_DOCKER_COMMAND} diff -r fixtures/ public/"
24 changes: 24 additions & 0 deletions tests/functional/site/fixtures/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex,nofollow,noarchive">
<title>An Error Occurred: Not Found</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>❌</text></svg>">
<style>body { background-color: #fff; color: #222; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; margin: 0; }
.container { margin: 30px; max-width: 600px; }
h1 { color: #dc3545; font-size: 24px; }
h2 { font-size: 18px; }</style>
</head>
<body>
<div class="container">
<h1>Oops! An Error Occurred</h1>
<h2>The server returned a "404 Not Found".</h2>

<p>
Something is broken. Please let us know what you were doing when this error occurred.
We will fix it as soon as possible. Sorry for any inconvenience caused.
</p>
</div>
</body>
</html>
Binary file not shown.
Binary file not shown.
Binary file added tests/functional/site/fixtures/sitemap-file-0.xml.gz
Binary file not shown.
3 changes: 3 additions & 0 deletions tests/functional/site/fixtures/sitemap-homepage-0.xml.gz
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
��M
�0F�"�3�n��'��qP!?����WS/�.
��|��#�՚셁F�*Vp�2t�w��+�h����J���lcUl�q�,��i�hۉ�=����A�+�RYI��{�qm�d�ko��'tc���8`�A�K�R��S�(��(�s�qKsJ{�DBr|%ŸD�~����
Binary file not shown.
1 change: 1 addition & 0 deletions tests/functional/site/fixtures/sitemap-product-0.xml.gz
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
��=�0 F���V���U[O@@����8�_�\�*Y�lOo�.g�F�/��e���+أ�� +���%Lٚ�T�>��0ϳ�!��G!v@M�����l����1G+�K�F�� hzB;D�}H=F@)aˏh�XmkJ.�&���*�gUIy���5wD4��o��Y�ݘ�n��g�
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-��
�0 F_Ez;l���F�w{w=j[��i���:!�����>&���#e�2AEm�ԑ��o�|MM�
�#s��`�6�6/�1M�j.¹F�:d�]Tb����+�� U��#h�@��g��XRԫ�����-�-�rT8�!�I�>jѲ�����6c��.G/܉p8n��}�
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/site/fixtures/sitemap.xml.gz
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
͔Ak� ��J�:�&�l������:�-8��`4��%���ڬ��1�@D�}|=���`�O�A;[�S�)+�Զ����9D���jࣶR-Y$,T ��!�!�<�y���HIiA�_���D�-n�BY�W�6N��n��!��u�P��ݑM����"|W�^�aƉ�Bd�‡�(,�@`� R{­ ��?x��s�c+�9��p��MI�"/�|W)��xHs̭F�տv8i�6V�ݠF�m��}�b��X-`c��;9��?,�Y���n
����'�Gb���j�
31 changes: 31 additions & 0 deletions tests/functional/site/src/Bridge/Symfony/EventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Sigwin Yassg project.
*
* (c) sigwin.hr
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sigwin\YASSG\Test\Functional\Site\Bridge\Symfony;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;

final class EventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return ['kernel.response' => 'onResponse'];
}

public function onResponse(ResponseEvent $event): void
{
$response = $event->getResponse();
$response->headers->add(['Last-Modified' => gmdate('D, d M Y H:i:s', strtotime('2021-12-31 00:00:00')).' GMT']);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

use PHPUnit\Framework\TestCase;
use Sigwin\YASSG\Bridge\Symfony\Routing\Generator\FilenameUrlGenerator;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
Expand All @@ -31,9 +30,7 @@ final class FilenameUrlGeneratorTest extends TestCase
{
public function testCannotGenerateUnknownRoute(): void
{
$this->expectException(RouteNotFoundException::class);

$generator = new FilenameUrlGenerator($this->getMockBuilder(UrlGeneratorInterface::class)->getMock(), [], []);
$generator->generate('unknown');
self::assertEmpty($generator->generate('unknown'));
}
}
Loading

0 comments on commit 117fbf4

Please sign in to comment.