Skip to content
Draft
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
20 changes: 20 additions & 0 deletions packages/realtime-compiler/src/Http/VirtualRouteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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',
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@ public function boot(): void
if (LiveEditController::enabled()) {
$router->registerVirtualRoute('/_hyde/live-edit', [VirtualRouteController::class, 'liveEdit']);
}

// 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().
}
}
39 changes: 39 additions & 0 deletions packages/realtime-compiler/src/Routing/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -34,6 +37,8 @@ public function handle(): Response

$this->overrideSiteUrl();

$this->registerDynamicVirtualRoutes();

$virtualRoutes = app(RealtimeCompiler::class)->getVirtualRoutes();

if (isset($virtualRoutes[$this->request->path])) {
Expand All @@ -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.
Expand All @@ -69,6 +95,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;
}
Expand Down
22 changes: 22 additions & 0 deletions packages/realtime-compiler/tests/Integration/IntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,26 @@ public function testDynamicDocumentationSearchPages()
unlink($this->projectPath('_docs/index.md'));
unlink($this->projectPath('_docs/installation.md'));
}

public function testDynamicSitemapGeneration()
{
// 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');
}

public function testDynamicRssFeedGeneration()
{
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('_posts/dynamic-rss-test.md'));
}
}
37 changes: 37 additions & 0 deletions packages/realtime-compiler/tests/RealtimeCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,43 @@ public function testGetContentTypeDefaultsToTextHtmlForUnknownExtension()
$this->assertSame('text/html', $this->invokeGetContentType($page));
}

public function testSitemapRouteReturnsSitemapResponse()
{
// 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');

$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('<?xml version="1.0" encoding="UTF-8"?>', $response->body);
$this->assertStringContainsString('<urlset', $response->body);
}

public function testRssFeedRouteReturnsRssResponse()
{
$this->mockCompilerRoute('feed.xml');
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('<?xml version="1.0" encoding="UTF-8"?>', $response->body);
$this->assertStringContainsString('<rss ', $response->body);
$this->assertStringContainsString('version="2.0"', $response->body);
$this->assertStringContainsString('Test Post', $response->body);

Filesystem::unlink('_posts/test-post.md');
}

public function testPingRouteReturnsPingResponse()
{
$this->mockCompilerRoute('ping');
Expand Down
Loading