diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 88c857db8e2..c152c7a480a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -27,6 +27,8 @@ This serves two purposes: - Minor: Data collection files are now validated for syntax errors during discovery in https://github.com/hydephp/develop/pull/1732 - Minor: Methods in the `Includes` facade now return `HtmlString` objects instead of `string` in https://github.com/hydephp/develop/pull/1738. For more information, see below. - Minor: `Includes::path()` and `Includes::get()` methods now normalizes paths to be basenames to match the behaviour of the other include methods in https://github.com/hydephp/develop/pull/1738. This means that nested directories are no longer supported, as you should use a data collection for that. +- Minor: The `processing_time_ms` attribute in the `sitemap.xml` file has now been removed in https://github.com/hydephp/develop/pull/1744 +- Improved the sitemap data generation to be smarter and more dynamic in https://github.com/hydephp/develop/pull/1744 - The `hasFeature` method on the Hyde facade and HydeKernel now only accepts a Feature enum value instead of a string for its parameter. - Changed how the documentation search is generated, to be an `InMemoryPage` instead of a post-build task. - Media asset files are now copied using the new build task instead of the deprecated `BuildService::transferMediaAssets()` method. diff --git a/packages/framework/src/Framework/Features/XmlGenerators/SitemapGenerator.php b/packages/framework/src/Framework/Features/XmlGenerators/SitemapGenerator.php index 38e53a3aa3b..01b3f2f10a8 100644 --- a/packages/framework/src/Framework/Features/XmlGenerators/SitemapGenerator.php +++ b/packages/framework/src/Framework/Features/XmlGenerators/SitemapGenerator.php @@ -8,29 +8,25 @@ use Hyde\Hyde; use SimpleXMLElement; -use Hyde\Facades\Config; +use Hyde\Pages\HtmlPage; use Hyde\Pages\BladePage; use Hyde\Pages\MarkdownPage; use Hyde\Pages\MarkdownPost; +use Hyde\Facades\Filesystem; +use Hyde\Pages\InMemoryPage; use Hyde\Support\Models\Route; +use Illuminate\Support\Carbon; use Hyde\Pages\DocumentationPage; use Hyde\Foundation\Facades\Routes; -use Hyde\Framework\Concerns\TracksExecutionTime; -use function blank; -use function filemtime; use function in_array; use function date; -use function time; -use function str_starts_with; /** * @see https://www.sitemaps.org/protocol.html */ class SitemapGenerator extends BaseXmlGenerator { - use TracksExecutionTime; - public function generate(): static { Routes::all()->each(function (Route $route): void { @@ -40,17 +36,8 @@ public function generate(): static return $this; } - public function getXml(): string - { - $this->xmlElement->addAttribute('processing_time_ms', $this->getFormattedProcessingTime()); - - return parent::getXml(); - } - protected function constructBaseElement(): void { - $this->startClock(); - $this->xmlElement = new SimpleXMLElement(''); $this->xmlElement->addAttribute('generator', 'HydePHP '.Hyde::version()); } @@ -61,62 +48,69 @@ protected function addRoute(Route $route): void $this->addChild($urlItem, 'loc', $this->resolveRouteLink($route)); $this->addChild($urlItem, 'lastmod', $this->getLastModDate($route->getSourcePath())); - $this->addChild($urlItem, 'changefreq', 'daily'); + $this->addChild($urlItem, 'changefreq', $this->generateChangeFrequency(...$this->getRouteInformation($route))); + $this->addChild($urlItem, 'priority', $this->generatePriority(...$this->getRouteInformation($route))); + } - if (Config::getBool('hyde.sitemap.dynamic_priority', true)) { - $this->addChild($urlItem, 'priority', $this->getPriority( - $route->getPageClass(), $route->getPage()->getIdentifier() - )); - } + protected function resolveRouteLink(Route $route): string + { + return Hyde::url($route->getOutputPath()); } protected function getLastModDate(string $file): string { - return date('c', @filemtime($file) ?: time()); + return date('c', @Filesystem::lastModified($file) ?: Carbon::now()->timestamp); } - protected function getPriority(string $pageClass, string $slug): string + /** + * @param class-string<\Hyde\Pages\Concerns\HydePage> $pageClass + * @return numeric-string + */ + protected function generatePriority(string $pageClass, string $identifier): string { $priority = 0.5; - if (in_array($pageClass, [BladePage::class, MarkdownPage::class])) { + if (in_array($pageClass, [BladePage::class, MarkdownPage::class, DocumentationPage::class])) { $priority = 0.9; - if ($slug === 'index') { + + if ($identifier === 'index') { $priority = 1; } - if ($slug === '404') { - $priority = 0.5; - } } - if ($pageClass === DocumentationPage::class) { - $priority = 0.9; + if (in_array($pageClass, [MarkdownPost::class, InMemoryPage::class, HtmlPage::class])) { + $priority = 0.75; } - if ($pageClass === MarkdownPost::class) { - $priority = 0.75; + if ($identifier === '404') { + $priority = 0.25; } return (string) $priority; } - /** @return numeric-string */ - protected function getFormattedProcessingTime(): string - { - return (string) $this->getExecutionTimeInMs(); - } - - protected function resolveRouteLink(Route $route): string + /** + * @param class-string<\Hyde\Pages\Concerns\HydePage> $pageClass + * @return 'always'|'hourly'|'daily '|'weekly'|'monthly'|'yearly'|'never' + */ + protected function generateChangeFrequency(string $pageClass, string $identifier): string { - $baseUrl = Config::getNullableString('hyde.url'); + $frequency = 'weekly'; - if (blank($baseUrl) || str_starts_with($baseUrl, 'http://localhost')) { - // While the sitemap spec requires a full URL, we rather fall back - // to using relative links instead of using localhost links. + if (in_array($pageClass, [BladePage::class, MarkdownPage::class, DocumentationPage::class])) { + $frequency = 'daily'; + } - return $route->getLink(); - } else { - return Hyde::url($route->getOutputPath()); + if ($identifier === '404') { + $frequency = 'monthly'; } + + return $frequency; + } + + /** @return array{class-string<\Hyde\Pages\Concerns\HydePage>, string} */ + protected function getRouteInformation(Route $route): array + { + return [$route->getPageClass(), $route->getPage()->getIdentifier()]; } } diff --git a/packages/framework/tests/Feature/Commands/BuildSitemapCommandTest.php b/packages/framework/tests/Feature/Commands/BuildSitemapCommandTest.php index 518dbb41796..ee13bc9bf03 100644 --- a/packages/framework/tests/Feature/Commands/BuildSitemapCommandTest.php +++ b/packages/framework/tests/Feature/Commands/BuildSitemapCommandTest.php @@ -4,7 +4,6 @@ namespace Hyde\Framework\Testing\Feature\Commands; -use Hyde\Facades\Filesystem; use Hyde\Hyde; use Hyde\Testing\TestCase; @@ -16,14 +15,33 @@ class BuildSitemapCommandTest extends TestCase { public function testSitemapIsGeneratedWhenConditionsAreMet() { - $this->withSiteUrl(); - config(['hyde.generate_sitemap' => true]); + config(['hyde.url' => 'https://example.com']); + + $this->cleanUpWhenDone('_site/sitemap.xml'); $this->assertFileDoesNotExist(Hyde::path('_site/sitemap.xml')); - $this->artisan('build:sitemap')->assertExitCode(0); + $this->artisan('build:sitemap') + ->expectsOutputToContain('Generating sitemap...') + ->doesntExpectOutputToContain('Skipped') + ->expectsOutputToContain(' > Created _site/sitemap.xml') + ->assertExitCode(0); + $this->assertFileExists(Hyde::path('_site/sitemap.xml')); + } + + public function testSitemapIsNotGeneratedWhenConditionsAreNotMet() + { + config(['hyde.url' => '']); - Filesystem::unlink('_site/sitemap.xml'); + $this->assertFileDoesNotExist(Hyde::path('_site/sitemap.xml')); + + $this->artisan('build:sitemap') + ->expectsOutputToContain('Generating sitemap...') + ->expectsOutputToContain('Skipped') + ->expectsOutput(' > Cannot generate sitemap without a valid base URL') + ->assertExitCode(0); + + $this->assertFileDoesNotExist(Hyde::path('_site/sitemap.xml')); } } diff --git a/packages/framework/tests/Feature/SitemapFeatureTest.php b/packages/framework/tests/Feature/SitemapFeatureTest.php new file mode 100644 index 00000000000..e0825f18013 --- /dev/null +++ b/packages/framework/tests/Feature/SitemapFeatureTest.php @@ -0,0 +1,157 @@ +makePartial(); + $filesystem->shouldReceive('lastModified')->andReturn(Carbon::now()->timestamp); + File::swap($filesystem); + + $this->cleanUpWhenDone('_site/sitemap.xml'); + $this->setUpBroadSiteStructure(); + $this->withSiteUrl(); + + $this->artisan('build:sitemap') + ->expectsOutputToContain('Created _site/sitemap.xml') + ->assertExitCode(0); + + $this->assertFileExists('_site/sitemap.xml'); + + $this->assertSameXml( + ''."\n{$this->stripFormatting($this->expected(Hyde::version()))}\n", + file_get_contents('_site/sitemap.xml') + ); + } + + protected function setUpBroadSiteStructure(): void + { + $this->file('_pages/about.md', "# About\n\nThis is the about page."); + $this->file('_pages/contact.html', '

Contact

This is the contact page.

'); + $this->file('_posts/hello-world.md', "# Hello, World!\n\nThis is the first post."); + $this->file('_posts/second-post.md', "# Second Post\n\nThis is the second post."); + $this->file('_docs/index.md', "# Documentation\n\nThis is the documentation index."); + $this->file('_docs/installation.md', "# Installation\n\nThis is the installation guide."); + $this->file('_docs/usage.md', "# Usage\n\nThis is the usage guide."); + $this->file('_docs/404.md', "# 404\n\nThis is the 404 page."); + } + + protected function expected(string $version): string + { + return << + + https://example.com/contact.html + 2024-01-01T12:00:00+00:00 + weekly + 0.75 + + + https://example.com/404.html + 2024-01-01T12:00:00+00:00 + monthly + 0.25 + + + https://example.com/index.html + 2024-01-01T12:00:00+00:00 + daily + 1 + + + https://example.com/about.html + 2024-01-01T12:00:00+00:00 + daily + 0.9 + + + https://example.com/posts/hello-world.html + 2024-01-01T12:00:00+00:00 + weekly + 0.75 + + + https://example.com/posts/second-post.html + 2024-01-01T12:00:00+00:00 + weekly + 0.75 + + + https://example.com/docs/404.html + 2024-01-01T12:00:00+00:00 + monthly + 0.25 + + + https://example.com/docs/index.html + 2024-01-01T12:00:00+00:00 + daily + 1 + + + https://example.com/docs/installation.html + 2024-01-01T12:00:00+00:00 + daily + 0.9 + + + https://example.com/docs/usage.html + 2024-01-01T12:00:00+00:00 + daily + 0.9 + + + https://example.com/docs/search.json + 2024-01-01T12:00:00+00:00 + weekly + 0.5 + + + https://example.com/docs/search.html + 2024-01-01T12:00:00+00:00 + weekly + 0.5 + + + XML; + } + + protected function stripFormatting(string $xml): string + { + return implode('', array_map('trim', explode("\n", $xml))); + } + + protected function expandLines(string $xml): string + { + return str_replace('><', ">\n<", $xml); + } + + protected function assertSameXml(string $expected, string $actual): void + { + $this->assertSame($this->expandLines($expected), $this->expandLines($actual)); + } +}