Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions bin/sitemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env php
<?php

declare(strict_types=1);

use Light\App\Service\SitemapGenerator;

chdir(__DIR__ . '/../');

require 'vendor/autoload.php';

$container = require 'config/container.php';
$sitemapGenerator = $container->get(SitemapGenerator::class);

$count = $sitemapGenerator->write();

printf(
"Done. %d url%s written to %s%s",
$count,
$count === 1 ? '' : 's',
$sitemapGenerator->getSitemapFile(),
PHP_EOL
);
5 changes: 4 additions & 1 deletion config/autoload/local.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,14 @@ return [
* scopes at all; without a token the generator still runs, at a lower rate limit.
* Non-credential settings (ignore list, output path) live in `packages.global.php`.
*/
'github' => [
'github' => [
'userAgent' => 'dotkernel.com',
'authBearer' => '',
'org' => 'dotkernel',
],
'sitemap' => [
'path' => realpath(__DIR__ . '/../../public/sitemap.xml'),
],
// `dotkernel-packages-oss-lifecycle` is intentionally absent: it has a dedicated handler
// and is routed in Light\App\RoutesDelegator. Re-adding it here registers the path twice.
'routes' => [
Expand Down
Empty file added public/sitemap.xml
Empty file.
6 changes: 6 additions & 0 deletions src/App/src/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,21 @@
use Light\App\Factory\GetIndexViewHandlerFactory;
use Light\App\Factory\GetMarkdownArticleHandlerFactory;
use Light\App\Factory\GetPackagesViewHandlerFactory;
use Light\App\Factory\GetSitemapViewHandlerFactory;
use Light\App\Factory\GitHubClientFactory;
use Light\App\Factory\PackageGeneratorFactory;
use Light\App\Factory\SitemapGeneratorFactory;
use Light\App\Handler\GetFeedViewHandler;
use Light\App\Handler\GetIndexViewHandler;
use Light\App\Handler\GetMarkdownArticleHandler;
use Light\App\Handler\GetPackagesViewHandler;
use Light\App\Handler\GetSitemapViewHandler;
use Light\App\Resolver\EntityListenerResolver;
use Light\App\Service\FeedGenerator;
use Light\App\Service\GitHubClient;
use Light\App\Service\GitHubClientInterface;
use Light\App\Service\PackageGenerator;
use Light\App\Service\SitemapGenerator;
use Mezzio\Application;
use Roave\PsrContainerDoctrine\EntityManagerFactory;
use Symfony\Component\Cache\Adapter\AdapterInterface;
Expand Down Expand Up @@ -128,8 +132,10 @@ public function getDependencies(): array
GetIndexViewHandler::class => GetIndexViewHandlerFactory::class,
GetFeedViewHandler::class => GetFeedViewHandlerFactory::class,
GetMarkdownArticleHandler::class => GetMarkdownArticleHandlerFactory::class,
GetSitemapViewHandler::class => GetSitemapViewHandlerFactory::class,
GetPackagesViewHandler::class => GetPackagesViewHandlerFactory::class,
FeedGenerator::class => FeedGeneratorFactory::class,
SitemapGenerator::class => SitemapGeneratorFactory::class,
GitHubClient::class => GitHubClientFactory::class,
PackageGenerator::class => PackageGeneratorFactory::class,
],
Expand Down
3 changes: 1 addition & 2 deletions src/App/src/Factory/FeedGeneratorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Psr\Container\ContainerInterface;

use function assert;
use function rtrim;

class FeedGeneratorFactory
{
Expand All @@ -23,7 +22,7 @@ public function __invoke(ContainerInterface $container): FeedGenerator
return new FeedGenerator(
$postRepository,
$config['feed']['path'],
rtrim($config['application']['baseUrl'] ?? '', '/') . '/',
$config['application']['baseUrl'] ?? '',
$config['application']['meta']['title'] ?? '',
$config['application']['meta']['description'] ?? '',
$config['application']['meta']['image'] ?? '',
Expand Down
33 changes: 33 additions & 0 deletions src/App/src/Factory/GetSitemapViewHandlerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Light\App\Factory;

use Light\App\Handler\GetSitemapViewHandler;
use Light\App\Service\SitemapGenerator;
use Light\Blog\Repository\CategoryRepository;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Container\ContainerInterface;

use function assert;

class GetSitemapViewHandlerFactory
{
/**
* @param class-string $requestedName
*/
public function __invoke(ContainerInterface $container, string $requestedName): GetSitemapViewHandler
{
$template = $container->get(TemplateRendererInterface::class);
assert($template instanceof TemplateRendererInterface);

$categoryRepository = $container->get(CategoryRepository::class);
assert($categoryRepository instanceof CategoryRepository);

$sitemapGenerator = $container->get(SitemapGenerator::class);
assert($sitemapGenerator instanceof SitemapGenerator);

return new GetSitemapViewHandler($template, $categoryRepository, $sitemapGenerator);
}
}
28 changes: 28 additions & 0 deletions src/App/src/Factory/SitemapGeneratorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Light\App\Factory;

use Light\App\Service\SitemapGenerator;
use Light\Blog\Repository\PostRepository;
use Psr\Container\ContainerInterface;

use function assert;

class SitemapGeneratorFactory
{
public function __invoke(ContainerInterface $container): SitemapGenerator
{
$postRepository = $container->get(PostRepository::class);
assert($postRepository instanceof PostRepository);

$config = $container->get('config');

return new SitemapGenerator(
$postRepository,
$config['sitemap']['path'],
$config['application']['baseUrl'] ?? '',
);
}
}
66 changes: 66 additions & 0 deletions src/App/src/Handler/GetSitemapViewHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Light\App\Handler;

use Fig\Http\Message\StatusCodeInterface;
use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\Diactoros\Response\XmlResponse;
use Light\App\Service\SitemapGenerator;
use Light\Blog\Entity\Category;
use Light\Blog\Repository\CategoryRepository;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;

use function file_get_contents;
use function filesize;
use function is_file;

class GetSitemapViewHandler implements RequestHandlerInterface
{
public function __construct(
private readonly TemplateRendererInterface $template,
private readonly CategoryRepository $categoryRepository,
private readonly SitemapGenerator $sitemapGenerator,
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
$sitemapFile = $this->sitemapGenerator->getSitemapFile();

if (! is_file($sitemapFile) || filesize($sitemapFile) === 0) {
try {
$this->sitemapGenerator->write();
} catch (Throwable) {
}
}

if (! is_file($sitemapFile) || filesize($sitemapFile) === 0) {
return $this->notFound($this->categoryRepository->getCategories());
}

return new XmlResponse(
(string) file_get_contents($sitemapFile),
StatusCodeInterface::STATUS_OK,
['Content-Type' => SitemapGenerator::CONTENT_TYPE]
);
}

/**
* @param Category[] $categories
*/
private function notFound(array $categories): HtmlResponse
{
return new HtmlResponse(
$this->template->render('error::404', [
'categories' => $categories,
]),
StatusCodeInterface::STATUS_NOT_FOUND
);
}
}
2 changes: 2 additions & 0 deletions src/App/src/RoutesDelegator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Light\App\Handler\GetIndexViewHandler;
use Light\App\Handler\GetMarkdownArticleHandler;
use Light\App\Handler\GetPackagesViewHandler;
use Light\App\Handler\GetSitemapViewHandler;
use Mezzio\Application;
use Psr\Container\ContainerInterface;

Expand All @@ -22,6 +23,7 @@ public function __invoke(ContainerInterface $container, string $serviceName, cal
assert($app instanceof Application);
$app->get('/', [GetIndexViewHandler::class], 'app::index');
$app->get('/feed/', [GetFeedViewHandler::class], 'app::feed');
$app->get('/sitemap/', [GetSitemapViewHandler::class], 'app::sitemap');
$app->get('/{categorySlug}/{slug}.md', [GetMarkdownArticleHandler::class], 'app::markdown-article');

// Route name kept as `page::…` because `@layout/default.html.twig` links it by name.
Expand Down
2 changes: 1 addition & 1 deletion src/App/src/Service/FeedGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function write(): int
$this->appendText($dom, $channel, 'lastBuildDate', (new DateTimeImmutable())->format(DateTimeInterface::RSS));

foreach ($posts as $post) {
$link = $this->baseUrl . $post->getCategory()->getSlug() . '/' . $post->getSlug() . '/';
$link = $this->baseUrl . '/' . $post->getCategory()->getSlug() . '/' . $post->getSlug() . '/';

$item = $dom->createElement('item');
$channel->appendChild($item);
Expand Down
74 changes: 74 additions & 0 deletions src/App/src/Service/SitemapGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace Light\App\Service;

use DateTimeInterface;
use DOMDocument;
use DOMElement;
use Light\Blog\Repository\PostRepository;
use RuntimeException;

use function count;

class SitemapGenerator
{
public const CONTENT_TYPE = 'application/rss+xml; charset=UTF-8';

private const SITEMAP_NAMESPACE = 'http://www.sitemaps.org/schemas/sitemap/0.9';

public function __construct(
private readonly PostRepository $postRepository,
private readonly string $sitemapFile,
private readonly string $baseUrl,
) {
}

public function getSitemapFile(): string
{
return $this->sitemapFile;
}

public function write(): int
{
$posts = $this->postRepository->getPublishedPosts();

$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;

$urlset = $dom->createElementNS(self::SITEMAP_NAMESPACE, 'urlset');
$dom->appendChild($urlset);

$this->appendUrl($dom, $urlset, $this->baseUrl);

foreach ($posts as $post) {
$link = $this->baseUrl . '/' . $post->getCategory()->getSlug() . '/' . $post->getSlug() . '/';
$this->appendUrl($dom, $urlset, $link, $post->getPostDate()->format(DateTimeInterface::W3C));
}

if ($dom->save($this->sitemapFile) === false) {
throw new RuntimeException('Unable to write sitemap.');
}

return count($posts) + 1;
}

private function appendUrl(DOMDocument $dom, DOMElement $urlset, string $loc, ?string $lastmod = null): void
{
$url = $dom->createElement('url');
$urlset->appendChild($url);

$this->appendText($dom, $url, 'loc', $loc);
if ($lastmod !== null) {
$this->appendText($dom, $url, 'lastmod', $lastmod);
}
}

private function appendText(DOMDocument $dom, DOMElement $parent, string $name, string $text): void
{
$el = $dom->createElement($name);
$el->appendChild($dom->createTextNode($text));
$parent->appendChild($el);
}
}
2 changes: 1 addition & 1 deletion src/App/templates/JSON-LD/index.jsonld.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"@graph": [
{
"@type": "WebSite",
"@id": "{{ absolute_url('/') }}#website",
"@id": "{{ absolute_url('/') }}#website ",
"url": "{{ absolute_url('/') }}",
"name": "Dotkernel",
"description": "Dotkernel is a collection of open-source application skeletons built on Mezzio and Laminas - pre-configured and ready for anything from a presentation site to an enterprise-grade API.",
Expand Down