From 26a7cf3363081b09b874bb54e19a61b38a2ac91d Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Sat, 4 Jul 2026 01:27:24 +0200 Subject: [PATCH 1/2] Update realtime compiler to serve sitemap and RSS feed --- .../src/Http/VirtualRouteController.php | 20 ++++++++++ .../src/RealtimeCompilerServiceProvider.php | 10 +++++ .../realtime-compiler/src/Routing/Router.php | 13 ++++++ .../tests/Integration/IntegrationTest.php | 28 +++++++++++++ .../tests/RealtimeCompilerTest.php | 40 +++++++++++++++++++ 5 files changed, 111 insertions(+) diff --git a/packages/realtime-compiler/src/Http/VirtualRouteController.php b/packages/realtime-compiler/src/Http/VirtualRouteController.php index 4b6e76ce546..dfc3a5ef7bc 100644 --- a/packages/realtime-compiler/src/Http/VirtualRouteController.php +++ b/packages/realtime-compiler/src/Http/VirtualRouteController.php @@ -7,6 +7,8 @@ use Desilva\Microserve\Request; use Desilva\Microserve\Response; use Desilva\Microserve\JsonResponse; +use Hyde\Framework\Features\XmlGenerators\SitemapGenerator; +use Hyde\Framework\Features\XmlGenerators\RssFeedGenerator; class VirtualRouteController { @@ -26,4 +28,22 @@ public static function liveEdit(Request $request): Response { return (new LiveEditController($request))->handle(); } + + public static function sitemap(): Response + { + return (new Response(200, 'OK', [ + 'body' => SitemapGenerator::make(), + ]))->withHeaders([ + 'Content-Type' => 'application/xml', + ]); + } + + public static function rssFeed(): Response + { + return (new Response(200, 'OK', [ + 'body' => RssFeedGenerator::make(), + ]))->withHeaders([ + 'Content-Type' => 'application/rss+xml', + ]); + } } diff --git a/packages/realtime-compiler/src/RealtimeCompilerServiceProvider.php b/packages/realtime-compiler/src/RealtimeCompilerServiceProvider.php index e5c77e18fa7..d32011db82f 100644 --- a/packages/realtime-compiler/src/RealtimeCompilerServiceProvider.php +++ b/packages/realtime-compiler/src/RealtimeCompilerServiceProvider.php @@ -5,11 +5,13 @@ namespace Hyde\RealtimeCompiler; use Illuminate\Support\ServiceProvider; +use Hyde\Facades\Features; use Hyde\RealtimeCompiler\Http\DashboardController; use Hyde\RealtimeCompiler\Http\LiveEditController; use Hyde\RealtimeCompiler\Http\VirtualRouteController; use Hyde\RealtimeCompiler\Console\Commands\HerdInstallCommand; use Hyde\RealtimeCompiler\Console\Commands\ServeCommand; +use Hyde\Framework\Features\XmlGenerators\RssFeedGenerator; class RealtimeCompilerServiceProvider extends ServiceProvider { @@ -38,5 +40,13 @@ public function boot(): void if (LiveEditController::enabled()) { $router->registerVirtualRoute('/_hyde/live-edit', [VirtualRouteController::class, 'liveEdit']); } + + if (Features::hasSitemap()) { + $router->registerVirtualRoute('/sitemap.xml', [VirtualRouteController::class, 'sitemap']); + } + + if (Features::hasRss()) { + $router->registerVirtualRoute('/'.RssFeedGenerator::getFilename(), [VirtualRouteController::class, 'rssFeed']); + } } } diff --git a/packages/realtime-compiler/src/Routing/Router.php b/packages/realtime-compiler/src/Routing/Router.php index bf58946d4b3..a2b16e2b6cd 100644 --- a/packages/realtime-compiler/src/Routing/Router.php +++ b/packages/realtime-compiler/src/Routing/Router.php @@ -69,6 +69,19 @@ protected function shouldProxy(Request $request): bool return false; } + // Don't proxy the sitemap, as it's generated on the fly. + // Note that unlike the RSS feed below, the sitemap filename is not configurable. + if ($request->path === '/sitemap.xml') { + return false; + } + + // Don't proxy the RSS feed, as it's generated on the fly. + // We can't resolve the configured `hyde.rss.filename` here as the application + // is not booted yet, so we match against the default filename instead. + if ($request->path === '/feed.xml') { + return false; + } + // The page is not a web page, so we assume it should be proxied. return true; } diff --git a/packages/realtime-compiler/tests/Integration/IntegrationTest.php b/packages/realtime-compiler/tests/Integration/IntegrationTest.php index 302b42ebe7e..4b84b7f712f 100644 --- a/packages/realtime-compiler/tests/Integration/IntegrationTest.php +++ b/packages/realtime-compiler/tests/Integration/IntegrationTest.php @@ -86,4 +86,32 @@ public function testDynamicDocumentationSearchPages() unlink($this->projectPath('_docs/index.md')); unlink($this->projectPath('_docs/installation.md')); } + + public function testDynamicSitemapGeneration() + { + // Setting a site URL is required to enable the sitemap feature. The realtime compiler + // then overrides it with the local server address, which is what we assert against. + file_put_contents($this->projectPath('.env'), 'SITE_URL=https://hydephp.dev'); + + $this->get('/sitemap.xml') + ->assertStatus(200) + ->assertHeader('Content-Type', 'application/xml') + ->assertSeeText('http://localhost:8080'); + + unlink($this->projectPath('.env')); + } + + public function testDynamicRssFeedGeneration() + { + file_put_contents($this->projectPath('.env'), 'SITE_URL=https://hydephp.dev'); + file_put_contents($this->projectPath('_posts/dynamic-rss-test.md'), "---\ntitle: Dynamic RSS Test\ndescription: Dynamic RSS test description\n---\n\n# Dynamic RSS Test"); + + $this->get('/feed.xml') + ->assertStatus(200) + ->assertHeader('Content-Type', 'application/rss+xml') + ->assertSeeText('Dynamic RSS Test'); + + unlink($this->projectPath('.env')); + unlink($this->projectPath('_posts/dynamic-rss-test.md')); + } } diff --git a/packages/realtime-compiler/tests/RealtimeCompilerTest.php b/packages/realtime-compiler/tests/RealtimeCompilerTest.php index 01b6e81d8fc..41224f7145d 100644 --- a/packages/realtime-compiler/tests/RealtimeCompilerTest.php +++ b/packages/realtime-compiler/tests/RealtimeCompilerTest.php @@ -256,6 +256,46 @@ public function testGetContentTypeDefaultsToTextHtmlForUnknownExtension() $this->assertSame('text/html', $this->invokeGetContentType($page)); } + public function testSitemapRouteReturnsSitemapResponse() + { + // The application is freshly booted within the router, so we need to set the + // site URL via the environment, as an in-memory config change won't be seen. + $this->mockCompilerRoute('sitemap.xml'); + putenv('SITE_URL=https://example.com'); + + $kernel = new HttpKernel(); + $response = $kernel->handle(new Request()); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->statusCode); + $this->assertSame('OK', $response->statusMessage); + $this->assertStringContainsString('', $response->body); + $this->assertStringContainsString('body); + + putenv('SITE_URL'); + } + + public function testRssFeedRouteReturnsRssResponse() + { + $this->mockCompilerRoute('feed.xml'); + putenv('SITE_URL=https://example.com'); + Filesystem::put('_posts/test-post.md', "---\ntitle: Test Post\ndescription: Test post description\n---\n\n# Test Post"); + + $kernel = new HttpKernel(); + $response = $kernel->handle(new Request()); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->statusCode); + $this->assertSame('OK', $response->statusMessage); + $this->assertStringContainsString('', $response->body); + $this->assertStringContainsString('body); + $this->assertStringContainsString('version="2.0"', $response->body); + $this->assertStringContainsString('Test Post', $response->body); + + Filesystem::unlink('_posts/test-post.md'); + putenv('SITE_URL'); + } + public function testPingRouteReturnsPingResponse() { $this->mockCompilerRoute('ping'); From 9b178e12d38b41e6ce84c885b8efa689637e1393 Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Sat, 4 Jul 2026 02:01:08 +0200 Subject: [PATCH 2/2] Update so we serve sitemap and feed even without site URL I feel like this gate is unnecessary for the local development --- .../src/RealtimeCompilerServiceProvider.php | 11 ++------ .../realtime-compiler/src/Routing/Router.php | 26 +++++++++++++++++++ .../tests/Integration/IntegrationTest.php | 10 ++----- .../tests/RealtimeCompilerTest.php | 11 +++----- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/realtime-compiler/src/RealtimeCompilerServiceProvider.php b/packages/realtime-compiler/src/RealtimeCompilerServiceProvider.php index d32011db82f..3854935e324 100644 --- a/packages/realtime-compiler/src/RealtimeCompilerServiceProvider.php +++ b/packages/realtime-compiler/src/RealtimeCompilerServiceProvider.php @@ -5,13 +5,11 @@ namespace Hyde\RealtimeCompiler; use Illuminate\Support\ServiceProvider; -use Hyde\Facades\Features; use Hyde\RealtimeCompiler\Http\DashboardController; use Hyde\RealtimeCompiler\Http\LiveEditController; use Hyde\RealtimeCompiler\Http\VirtualRouteController; use Hyde\RealtimeCompiler\Console\Commands\HerdInstallCommand; use Hyde\RealtimeCompiler\Console\Commands\ServeCommand; -use Hyde\Framework\Features\XmlGenerators\RssFeedGenerator; class RealtimeCompilerServiceProvider extends ServiceProvider { @@ -41,12 +39,7 @@ public function boot(): void $router->registerVirtualRoute('/_hyde/live-edit', [VirtualRouteController::class, 'liveEdit']); } - if (Features::hasSitemap()) { - $router->registerVirtualRoute('/sitemap.xml', [VirtualRouteController::class, 'sitemap']); - } - - if (Features::hasRss()) { - $router->registerVirtualRoute('/'.RssFeedGenerator::getFilename(), [VirtualRouteController::class, 'rssFeed']); - } + // The sitemap and RSS feed routes are registered in the Router itself, once the site URL + // has been finalized for the request. See Router::registerDynamicVirtualRoutes(). } } diff --git a/packages/realtime-compiler/src/Routing/Router.php b/packages/realtime-compiler/src/Routing/Router.php index a2b16e2b6cd..15f341ab045 100644 --- a/packages/realtime-compiler/src/Routing/Router.php +++ b/packages/realtime-compiler/src/Routing/Router.php @@ -6,11 +6,14 @@ use Desilva\Microserve\Request; use Desilva\Microserve\Response; +use Hyde\Facades\Features; use Hyde\RealtimeCompiler\RealtimeCompiler; use Hyde\RealtimeCompiler\Actions\AssetFileLocator; use Hyde\RealtimeCompiler\Concerns\SendsErrorResponses; +use Hyde\RealtimeCompiler\Http\VirtualRouteController; use Hyde\RealtimeCompiler\Models\FileObject; use Hyde\RealtimeCompiler\Concerns\InteractsWithLaravel; +use Hyde\Framework\Features\XmlGenerators\RssFeedGenerator; class Router { @@ -34,6 +37,8 @@ public function handle(): Response $this->overrideSiteUrl(); + $this->registerDynamicVirtualRoutes(); + $virtualRoutes = app(RealtimeCompiler::class)->getVirtualRoutes(); if (isset($virtualRoutes[$this->request->path])) { @@ -43,6 +48,27 @@ public function handle(): Response return PageRouter::handle($this->request); } + /** + * Register virtual routes whose availability depends on the site URL, which is only + * finalized after {@see overrideSiteUrl()} has run. Unlike the routes registered in + * the service provider's boot method, these can't be resolved any earlier: outside of + * `save_preview` mode, the site URL is always overridden to a local address, so (unlike + * a real `hyde build`) we don't need a production site URL to be configured to serve + * these on the local dev server. + */ + protected function registerDynamicVirtualRoutes(): void + { + $compiler = app(RealtimeCompiler::class); + + if (Features::hasSitemap()) { + $compiler->registerVirtualRoute('/sitemap.xml', [VirtualRouteController::class, 'sitemap']); + } + + if (Features::hasRss()) { + $compiler->registerVirtualRoute('/'.RssFeedGenerator::getFilename(), [VirtualRouteController::class, 'rssFeed']); + } + } + /** * If the request is not for a web page, we assume it's * a static asset, which we instead want to proxy. diff --git a/packages/realtime-compiler/tests/Integration/IntegrationTest.php b/packages/realtime-compiler/tests/Integration/IntegrationTest.php index 4b84b7f712f..f12bacca18f 100644 --- a/packages/realtime-compiler/tests/Integration/IntegrationTest.php +++ b/packages/realtime-compiler/tests/Integration/IntegrationTest.php @@ -89,21 +89,16 @@ public function testDynamicDocumentationSearchPages() public function testDynamicSitemapGeneration() { - // Setting a site URL is required to enable the sitemap feature. The realtime compiler - // then overrides it with the local server address, which is what we assert against. - file_put_contents($this->projectPath('.env'), 'SITE_URL=https://hydephp.dev'); - + // No production site URL needs to be configured: the realtime compiler always + // overrides it with the local server address, which is what we assert against. $this->get('/sitemap.xml') ->assertStatus(200) ->assertHeader('Content-Type', 'application/xml') ->assertSeeText('http://localhost:8080'); - - unlink($this->projectPath('.env')); } public function testDynamicRssFeedGeneration() { - file_put_contents($this->projectPath('.env'), 'SITE_URL=https://hydephp.dev'); file_put_contents($this->projectPath('_posts/dynamic-rss-test.md'), "---\ntitle: Dynamic RSS Test\ndescription: Dynamic RSS test description\n---\n\n# Dynamic RSS Test"); $this->get('/feed.xml') @@ -111,7 +106,6 @@ public function testDynamicRssFeedGeneration() ->assertHeader('Content-Type', 'application/rss+xml') ->assertSeeText('Dynamic RSS Test'); - unlink($this->projectPath('.env')); unlink($this->projectPath('_posts/dynamic-rss-test.md')); } } diff --git a/packages/realtime-compiler/tests/RealtimeCompilerTest.php b/packages/realtime-compiler/tests/RealtimeCompilerTest.php index 41224f7145d..d95aaf14251 100644 --- a/packages/realtime-compiler/tests/RealtimeCompilerTest.php +++ b/packages/realtime-compiler/tests/RealtimeCompilerTest.php @@ -258,10 +258,11 @@ public function testGetContentTypeDefaultsToTextHtmlForUnknownExtension() public function testSitemapRouteReturnsSitemapResponse() { - // The application is freshly booted within the router, so we need to set the - // site URL via the environment, as an in-memory config change won't be seen. + // Note this works even without a production site URL configured: the router always + // overrides the site URL to the local server address (unless save_preview is enabled), + // so the sitemap and RSS feed are available on the dev server regardless of whether a + // production URL has been set. $this->mockCompilerRoute('sitemap.xml'); - putenv('SITE_URL=https://example.com'); $kernel = new HttpKernel(); $response = $kernel->handle(new Request()); @@ -271,14 +272,11 @@ public function testSitemapRouteReturnsSitemapResponse() $this->assertSame('OK', $response->statusMessage); $this->assertStringContainsString('', $response->body); $this->assertStringContainsString('body); - - putenv('SITE_URL'); } public function testRssFeedRouteReturnsRssResponse() { $this->mockCompilerRoute('feed.xml'); - putenv('SITE_URL=https://example.com'); Filesystem::put('_posts/test-post.md', "---\ntitle: Test Post\ndescription: Test post description\n---\n\n# Test Post"); $kernel = new HttpKernel(); @@ -293,7 +291,6 @@ public function testRssFeedRouteReturnsRssResponse() $this->assertStringContainsString('Test Post', $response->body); Filesystem::unlink('_posts/test-post.md'); - putenv('SITE_URL'); } public function testPingRouteReturnsPingResponse()