From a47a75c8870dc1ec47e4dd439c46cc5be309ff2d Mon Sep 17 00:00:00 2001 From: OStefan2001 Date: Mon, 10 Aug 2026 14:58:23 +0300 Subject: [PATCH 1/3] Add missing category, author, static pages to sitemap. --- .../src/Factory/SitemapGeneratorFactory.php | 21 +++ src/App/src/Service/SitemapGenerator.php | 60 ++++++++- src/Blog/src/Repository/AuthorRepository.php | 29 +++- .../Unit/App/Service/SitemapGeneratorTest.php | 125 +++++++++++++++--- 4 files changed, 206 insertions(+), 29 deletions(-) diff --git a/src/App/src/Factory/SitemapGeneratorFactory.php b/src/App/src/Factory/SitemapGeneratorFactory.php index 488706ce..099afd34 100644 --- a/src/App/src/Factory/SitemapGeneratorFactory.php +++ b/src/App/src/Factory/SitemapGeneratorFactory.php @@ -5,10 +5,15 @@ namespace Light\App\Factory; use Light\App\Service\SitemapGenerator; +use Light\Blog\Repository\AuthorRepository; +use Light\Blog\Repository\CategoryRepository; use Light\Blog\Repository\PostRepository; use Psr\Container\ContainerInterface; +use function array_keys; +use function array_merge; use function assert; +use function is_array; class SitemapGeneratorFactory { @@ -17,10 +22,26 @@ public function __invoke(ContainerInterface $container): SitemapGenerator $postRepository = $container->get(PostRepository::class); assert($postRepository instanceof PostRepository); + $categoryRepository = $container->get(CategoryRepository::class); + assert($categoryRepository instanceof CategoryRepository); + + $authorRepository = $container->get(AuthorRepository::class); + assert($authorRepository instanceof AuthorRepository); + $config = $container->get('config'); + $pageRoutes = []; + foreach ($config['routes'] ?? [] as $moduleRoutes) { + if (is_array($moduleRoutes)) { + $pageRoutes = array_merge($pageRoutes, array_keys($moduleRoutes)); + } + } + return new SitemapGenerator( $postRepository, + $categoryRepository, + $authorRepository, + $pageRoutes, $config['sitemap']['path'], $config['application']['baseUrl'] ?? '', ); diff --git a/src/App/src/Service/SitemapGenerator.php b/src/App/src/Service/SitemapGenerator.php index 9fdbd825..bd39e03d 100644 --- a/src/App/src/Service/SitemapGenerator.php +++ b/src/App/src/Service/SitemapGenerator.php @@ -7,10 +7,12 @@ use DateTimeInterface; use DOMDocument; use DOMElement; +use Light\Blog\Repository\AuthorRepository; +use Light\Blog\Repository\CategoryRepository; use Light\Blog\Repository\PostRepository; use RuntimeException; -use function count; +use function sprintf; class SitemapGenerator { @@ -18,8 +20,15 @@ class SitemapGenerator private const SITEMAP_NAMESPACE = 'http://www.sitemaps.org/schemas/sitemap/0.9'; + /** + * @param array $pageRoutes Route URIs registered under config['routes'][*], + * e.g. ['contact'] for the /contact/ static page. + */ public function __construct( private readonly PostRepository $postRepository, + private readonly CategoryRepository $categoryRepository, + private readonly AuthorRepository $authorRepository, + private readonly array $pageRoutes, private readonly string $sitemapFile, private readonly string $baseUrl, ) { @@ -32,7 +41,9 @@ public function getSitemapFile(): string public function write(): int { - $posts = $this->postRepository->getPublishedPosts(); + $posts = $this->postRepository->getPublishedPosts(); + $categories = $this->categoryRepository->getCategories(); + $authors = $this->authorRepository->getAuthorsWithPublishedPosts(); $dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; @@ -40,18 +51,57 @@ public function write(): int $urlset = $dom->createElementNS(self::SITEMAP_NAMESPACE, 'urlset'); $dom->appendChild($urlset); - $this->appendUrl($dom, $urlset, $this->baseUrl); + $count = 0; + + $this->appendUrl($dom, $urlset, $this->baseUrl . '/'); + $count++; + + $this->appendUrl($dom, $urlset, $this->baseUrl . '/blog/'); + $count++; + + $this->appendUrl($dom, $urlset, $this->baseUrl . '/categories/'); + $count++; + + $this->appendUrl($dom, $urlset, $this->baseUrl . '/dotkernel-packages-oss-lifecycle/'); + $count++; + + foreach ($this->pageRoutes as $routeUri) { + $this->appendUrl($dom, $urlset, sprintf('%s/%s/', $this->baseUrl, $routeUri)); + $count++; + } + + foreach ($categories as $category) { + $lastmod = $category->getUpdated() ?? $category->getCreated(); + $this->appendUrl( + $dom, + $urlset, + sprintf('%s/category/%s/', $this->baseUrl, $category->getSlug()), + $lastmod?->format(DateTimeInterface::W3C) + ); + $count++; + } + + foreach ($authors as $author) { + $this->appendUrl($dom, $urlset, sprintf('%s/author/%s/', $this->baseUrl, $author->getSlug())); + $count++; + } foreach ($posts as $post) { - $link = $this->baseUrl . '/' . $post->getCategory()->getSlug() . '/' . $post->getSlug() . '/'; + $link = sprintf( + '%s/%s/%s/', + $this->baseUrl, + $post->getCategory()->getSlug(), + $post->getSlug() + ); $this->appendUrl($dom, $urlset, $link, $post->getPostDate()->format(DateTimeInterface::W3C)); + $count++; } if ($dom->save($this->sitemapFile) === false) { throw new RuntimeException('Unable to write sitemap.'); } - return count($posts) + 1; + return $count; } private function appendUrl(DOMDocument $dom, DOMElement $urlset, string $loc, ?string $lastmod = null): void diff --git a/src/Blog/src/Repository/AuthorRepository.php b/src/Blog/src/Repository/AuthorRepository.php index 410753a4..06709359 100644 --- a/src/Blog/src/Repository/AuthorRepository.php +++ b/src/Blog/src/Repository/AuthorRepository.php @@ -6,6 +6,8 @@ use Light\App\Repository\AbstractRepository; use Light\Blog\Entity\Author; +use Light\Blog\Entity\Post; +use Light\Blog\Enum\PostStatusEnum; class AuthorRepository extends AbstractRepository { @@ -15,8 +17,31 @@ class AuthorRepository extends AbstractRepository public function getAuthor(): array { $qb = $this->getQueryBuilder() - ->select('author.name, author.slug') - ->from(Author::class, 'authors'); + ->select('author') + ->from(Author::class, 'author'); + + return $qb->getQuery()->getResult(); + } + + /** + * Authors with at least one published post, i.e. authors whose page actually has content. + * + * @return array + */ + public function getAuthorsWithPublishedPosts(): array + { + $publishedAuthorIds = $this->getQueryBuilder() + ->select('publishedAuthor.id') + ->from(Post::class, 'post') + ->join('post.author', 'publishedAuthor') + ->where('post.status = :published'); + + $qb = $this->getQueryBuilder() + ->select('author') + ->from(Author::class, 'author'); + + $qb->where($qb->expr()->in('author.id', $publishedAuthorIds->getDQL())) + ->setParameter('published', PostStatusEnum::Published); return $qb->getQuery()->getResult(); } diff --git a/test/Unit/App/Service/SitemapGeneratorTest.php b/test/Unit/App/Service/SitemapGeneratorTest.php index 7ffc34b7..c9de1c43 100644 --- a/test/Unit/App/Service/SitemapGeneratorTest.php +++ b/test/Unit/App/Service/SitemapGeneratorTest.php @@ -7,8 +7,11 @@ use DateTimeImmutable; use DateTimeZone; use Light\App\Service\SitemapGenerator; +use Light\Blog\Entity\Author; use Light\Blog\Entity\Category; use Light\Blog\Entity\Post; +use Light\Blog\Repository\AuthorRepository; +use Light\Blog\Repository\CategoryRepository; use Light\Blog\Repository\PostRepository; use LightTest\Unit\UnitTest; use PHPUnit\Framework\MockObject\Exception; @@ -31,6 +34,9 @@ class SitemapGeneratorTest extends UnitTest { + /** Homepage, /blog/, /categories/ and the packages-lifecycle page are always present. */ + private const FIXED_URL_COUNT = 4; + private string $sitemapFile; protected function setUp(): void @@ -64,37 +70,76 @@ protected function tearDown(): void public function testGetSitemapFileReturnsTheConfiguredPath(): void { - $this->assertSame($this->sitemapFile, $this->createGenerator([])->getSitemapFile()); + $this->assertSame($this->sitemapFile, $this->createGenerator()->getSitemapFile()); } /** - * The count includes the homepage entry in addition to one entry per post. - * * @throws Exception */ - public function testWriteReturnsTheNumberOfPostsPlusTheHomepage(): void + public function testWriteAlwaysIncludesTheFixedPagesEvenWithoutAnyContent(): void { - $generator = $this->createGenerator([ - $this->createPost('first-post', 'news'), - $this->createPost('second-post', 'news'), - ]); + $generator = $this->createGenerator(); + + $this->assertSame(self::FIXED_URL_COUNT, $generator->write()); - $this->assertSame(3, $generator->write()); + $urls = $this->loadSitemap()->url; + $this->assertCount(self::FIXED_URL_COUNT, $urls); + $this->assertSame('https://example.test/', (string) $urls[0]->loc); + $this->assertSame('https://example.test/blog/', (string) $urls[1]->loc); + $this->assertSame('https://example.test/categories/', (string) $urls[2]->loc); + $this->assertSame( + 'https://example.test/dotkernel-packages-oss-lifecycle/', + (string) $urls[3]->loc + ); + $this->assertCount(0, $urls[0]->lastmod); } /** * @throws Exception */ - public function testWriteAlwaysIncludesTheHomepageEvenWithoutPosts(): void + public function testWriteAddsOneUrlEntryPerConfiguredStaticPage(): void { - $generator = $this->createGenerator([]); + $generator = $this->createGenerator(pageRoutes: ['contact']); - $this->assertSame(1, $generator->write()); + $this->assertSame(self::FIXED_URL_COUNT + 1, $generator->write()); $urls = $this->loadSitemap()->url; - $this->assertCount(1, $urls); - $this->assertSame('https://example.test', (string) $urls[0]->loc); - $this->assertCount(0, $urls[0]->lastmod); + $this->assertSame('https://example.test/contact/', (string) $urls[self::FIXED_URL_COUNT]->loc); + } + + /** + * @throws Exception + */ + public function testWriteAddsOneUrlEntryPerCategoryWithItsLastModifiedDate(): void + { + $category = $this->createCategory('news', '2026-08-01 10:00:00'); + $generator = $this->createGenerator(categories: [$category]); + + $this->assertSame(self::FIXED_URL_COUNT + 1, $generator->write()); + + $urls = $this->loadSitemap()->url; + $this->assertSame('https://example.test/category/news/', (string) $urls[self::FIXED_URL_COUNT]->loc); + $this->assertSame( + '2026-08-01T10:00:00+00:00', + (string) $urls[self::FIXED_URL_COUNT]->lastmod + ); + } + + /** + * @throws Exception + */ + public function testWriteAddsOneUrlEntryPerAuthor(): void + { + $author = $this->createStub(Author::class); + $author->method('getSlug')->willReturn('jane-doe'); + + $generator = $this->createGenerator(authors: [$author]); + + $this->assertSame(self::FIXED_URL_COUNT + 1, $generator->write()); + + $urls = $this->loadSitemap()->url; + $this->assertSame('https://example.test/author/jane-doe/', (string) $urls[self::FIXED_URL_COUNT]->loc); + $this->assertCount(0, $urls[self::FIXED_URL_COUNT]->lastmod); } /** @@ -103,13 +148,19 @@ public function testWriteAlwaysIncludesTheHomepageEvenWithoutPosts(): void public function testWriteAddsOneUrlEntryPerPostWithACategoryQualifiedLink(): void { $post = $this->createPost('a-post', 'news', '2026-08-01 10:00:00'); - $this->createGenerator([$post])->write(); + $this->createGenerator(posts: [$post])->write(); $urls = $this->loadSitemap()->url; - $this->assertCount(2, $urls); - $this->assertSame('https://example.test/news/a-post/', (string) $urls[1]->loc); - $this->assertSame('2026-08-01T10:00:00+00:00', (string) $urls[1]->lastmod); + $this->assertCount(self::FIXED_URL_COUNT + 1, $urls); + $this->assertSame( + 'https://example.test/news/a-post/', + (string) $urls[self::FIXED_URL_COUNT]->loc + ); + $this->assertSame( + '2026-08-01T10:00:00+00:00', + (string) $urls[self::FIXED_URL_COUNT]->lastmod + ); } /** @@ -120,7 +171,7 @@ public function testWriteAddsOneUrlEntryPerPostWithACategoryQualifiedLink(): voi */ public function testWriteThrowsWhenTheSitemapFileCannotBeWritten(): void { - $generator = $this->createGenerator([], sitemapFile: '/nonexistent-directory/sitemap.xml'); + $generator = $this->createGenerator(sitemapFile: '/nonexistent-directory/sitemap.xml'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Unable to write sitemap.'); @@ -130,15 +181,32 @@ public function testWriteThrowsWhenTheSitemapFileCannotBeWritten(): void /** * @param list $posts + * @param list $categories + * @param list $authors + * @param list $pageRoutes * @throws Exception */ - private function createGenerator(array $posts, ?string $sitemapFile = null): SitemapGenerator - { + private function createGenerator( + array $posts = [], + array $categories = [], + array $authors = [], + array $pageRoutes = [], + ?string $sitemapFile = null, + ): SitemapGenerator { $postRepository = $this->createStub(PostRepository::class); $postRepository->method('getPublishedPosts')->willReturn($posts); + $categoryRepository = $this->createStub(CategoryRepository::class); + $categoryRepository->method('getCategories')->willReturn($categories); + + $authorRepository = $this->createStub(AuthorRepository::class); + $authorRepository->method('getAuthorsWithPublishedPosts')->willReturn($authors); + return new SitemapGenerator( $postRepository, + $categoryRepository, + $authorRepository, + $pageRoutes, $sitemapFile ?? $this->sitemapFile, 'https://example.test', ); @@ -160,6 +228,19 @@ private function createPost(string $slug, string $categorySlug, string $postDate return $post; } + /** + * @throws Exception + */ + private function createCategory(string $slug, string $updated): Category + { + $category = $this->createStub(Category::class); + $category->method('getSlug')->willReturn($slug); + $category->method('getUpdated')->willReturn(new DateTimeImmutable($updated, new DateTimeZone('UTC'))); + $category->method('getCreated')->willReturn(new DateTimeImmutable($updated, new DateTimeZone('UTC'))); + + return $category; + } + private function loadSitemap(): SimpleXMLElement { $this->assertFileExists($this->sitemapFile); From 767f1304528e123d71cb3a05241f25446899366b Mon Sep 17 00:00:00 2001 From: OStefan2001 Date: Mon, 10 Aug 2026 15:00:59 +0300 Subject: [PATCH 2/3] Add missing category, author, static pages to sitemap. --- src/App/src/Service/SitemapGenerator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App/src/Service/SitemapGenerator.php b/src/App/src/Service/SitemapGenerator.php index bd39e03d..ea25b7ee 100644 --- a/src/App/src/Service/SitemapGenerator.php +++ b/src/App/src/Service/SitemapGenerator.php @@ -21,8 +21,8 @@ class SitemapGenerator private const SITEMAP_NAMESPACE = 'http://www.sitemaps.org/schemas/sitemap/0.9'; /** - * @param array $pageRoutes Route URIs registered under config['routes'][*], - * e.g. ['contact'] for the /contact/ static page. + * @param array $pageRoutes + * */ public function __construct( private readonly PostRepository $postRepository, From 6f35479797ff4a535ccb511df1e51f3bdeb7c85d Mon Sep 17 00:00:00 2001 From: OStefan2001 Date: Mon, 10 Aug 2026 15:05:27 +0300 Subject: [PATCH 3/3] Add missing category, author, static pages to sitemap. --- src/App/src/Service/SitemapGenerator.php | 1 - src/Blog/src/Repository/AuthorRepository.php | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/App/src/Service/SitemapGenerator.php b/src/App/src/Service/SitemapGenerator.php index ea25b7ee..1e9f34e5 100644 --- a/src/App/src/Service/SitemapGenerator.php +++ b/src/App/src/Service/SitemapGenerator.php @@ -22,7 +22,6 @@ class SitemapGenerator /** * @param array $pageRoutes - * */ public function __construct( private readonly PostRepository $postRepository, diff --git a/src/Blog/src/Repository/AuthorRepository.php b/src/Blog/src/Repository/AuthorRepository.php index 06709359..fe3b4cda 100644 --- a/src/Blog/src/Repository/AuthorRepository.php +++ b/src/Blog/src/Repository/AuthorRepository.php @@ -24,8 +24,6 @@ public function getAuthor(): array } /** - * Authors with at least one published post, i.e. authors whose page actually has content. - * * @return array */ public function getAuthorsWithPublishedPosts(): array