From b737c7a9f2ce54e99ec551631b0c28af357f41b3 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Fri, 14 Jul 2023 14:41:38 +0100 Subject: [PATCH] Support generating individual URLs (#137) Co-authored-by: Jesse Leite --- src/Commands/StaticSiteGenerate.php | 3 +- src/Generator.php | 81 ++++++++++++++++-------- tests/GenerateTest.php | 98 +++++++++++++++++++++++++++++ tests/Localized/GenerateTest.php | 61 ++++++++++++++++++ 4 files changed, 215 insertions(+), 28 deletions(-) diff --git a/src/Commands/StaticSiteGenerate.php b/src/Commands/StaticSiteGenerate.php index 3815955..054be48 100644 --- a/src/Commands/StaticSiteGenerate.php +++ b/src/Commands/StaticSiteGenerate.php @@ -23,6 +23,7 @@ class StaticSiteGenerate extends Command * @var string */ protected $signature = 'statamic:ssg:generate + {urls?* : You may provide one or more explicit url arguments, otherwise whole site will be generated } {--workers= : Speed up site generation significantly by installing spatie/fork and using multiple workers } {--disable-clear : Disable clearing the destination directory when generating whole site }'; @@ -62,7 +63,7 @@ public function handle() $this->generator ->workers($workers ?? 1) ->disableClear($this->option('disable-clear') ?? false) - ->generate(); + ->generate($this->argument('urls') ?: '*'); } catch (GenerationFailedException $e) { $this->line($e->getConsoleMessage()); $this->error('Static site generation failed.'); diff --git a/src/Generator.php b/src/Generator.php index c80f566..841bacc 100644 --- a/src/Generator.php +++ b/src/Generator.php @@ -35,6 +35,7 @@ class Generator protected $after; protected $extraUrls; protected $workers = 1; + protected $earlyTaskErrors = []; protected $taskResults; protected $disableClear = false; @@ -85,16 +86,20 @@ public function disableClear(bool $disableClear = false) return $this; } - public function generate() + public function generate($urls = '*') { $this->checkConcurrencySupport(); Site::setCurrent(Site::default()->handle()); + if (is_array($urls)) { + $this->disableClear = true; + } + $this ->bindGlide() ->clearDirectory() - ->createContentFiles() + ->createContentFiles($urls) ->createSymlinks() ->copyFiles() ->outputSummary(); @@ -178,7 +183,7 @@ public function copyFiles() return $this; } - protected function createContentFiles() + protected function createContentFiles($urls = '*') { $request = tap(Request::capture(), function ($request) { $request->setConfig($this->config); @@ -186,7 +191,7 @@ protected function createContentFiles() Cascade::withRequest($request); }); - $pages = $this->gatherContent(); + $pages = $this->gatherContent($urls); Partyline::line("Generating {$pages->count()} content files..."); @@ -215,24 +220,30 @@ protected function compileTasksResults(array $results) $results = collect($results); return [ - 'count' => $results->sum('count'), + 'count' => count($this->earlyTaskErrors) + $results->sum('count'), 'warnings' => $results->flatMap->warnings, - 'errors' => $results->flatMap->errors, + 'errors' => collect($this->earlyTaskErrors)->merge($results->flatMap->errors), ]; } - protected function gatherContent() + protected function gatherContent($urls = '*') { + if (is_array($urls)) { + return collect($urls) + ->map(fn ($url) => $this->createPage(new Route($this->makeAbsoluteUrl($url)))) + ->reject(fn ($page) => $this->shouldRejectPage($page, true)); + } + Partyline::line('Gathering content to be generated...'); - $pages = $this->pages(); + $pages = $this->gatherAllPages(); Partyline::line("\x1B[1A\x1B[2K[✔] Gathered content to be generated"); return $pages; } - protected function pages() + protected function gatherAllPages() { return collect() ->merge($this->routes()) @@ -241,18 +252,10 @@ protected function pages() ->merge($this->terms()) ->merge($this->scopedTerms()) ->values() - ->unique->url() - ->reject(function ($page) { - foreach ($this->config['exclude'] as $url) { - if (Str::endsWith($url, '*')) { - if (Str::is($url, $page->url())) { - return true; - } - } - } - - return in_array($page->url(), $this->config['exclude']); - })->shuffle(); + ->unique + ->url() + ->reject(fn ($page) => $this->shouldRejectPage($page)) + ->shuffle(); } protected function makeContentGenerationClosures($pages, $request) @@ -311,7 +314,9 @@ protected function outputTasksResults() { $results = $this->taskResults; - Partyline::line("\x1B[1A\x1B[2K[✔] Generated {$results['count']} content files"); + $successCount = $results['count'] - $results['errors']->count(); + + Partyline::line("\x1B[1A\x1B[2K[✔] Generated {$successCount} content files"); $results['warnings']->merge($results['errors'])->each(fn ($error) => Partyline::line($error)); } @@ -390,11 +395,9 @@ protected function urls() $extra[] = '/404'; } - return collect($this->config['urls'] ?? [])->merge($extra)->map(function ($url) { - $url = URL::tidy(Str::start($url, $this->config['base_url'].'/')); - - return $this->createPage(new Route($url)); - }); + return collect($this->config['urls'] ?? []) + ->merge($extra) + ->map(fn ($url) => $this->createPage(new Route($this->makeAbsoluteUrl($url)))); } protected function routes() @@ -453,4 +456,28 @@ protected function shouldSetCarbonFormat($page) || $content instanceof \Statamic\Contracts\Taxonomies\Term || $content instanceof StatamicRoute; } + + protected function makeAbsoluteUrl($url) + { + return URL::tidy(Str::start($url, $this->config['base_url'].'/')); + } + + protected function shouldRejectPage($page, $outputError = false) + { + foreach ($this->config['exclude'] as $url) { + if (Str::endsWith($url, '*')) { + if (Str::is($url, $page->url())) { + return true; + } + } + } + + $excluded = in_array($page->url(), $this->config['exclude']); + + if ($excluded && $outputError) { + $this->earlyTaskErrors[] = '[✘] '.URL::makeRelative($page->url()).' (Excluded in config/statamic/ssg.php)'; + } + + return $excluded; + } } diff --git a/tests/GenerateTest.php b/tests/GenerateTest.php index 7fce21f..1ea1e6d 100644 --- a/tests/GenerateTest.php +++ b/tests/GenerateTest.php @@ -59,6 +59,30 @@ public function it_generates_pages_for_site_fixture() $this->assertStringContainsString('

Article Title: Eight

', $files['articles/eight/index.html']); } + /** @test */ + public function it_generates_specific_pages_when_passing_urls_as_args() + { + $this + ->partialMock(Filesystem::class) + ->shouldReceive('deleteDirectory') + ->with(config('statamic.ssg.destination'), true) + ->never(); + + $files = $this->generate(['urls' => ['/', 'topics', 'articles']]); + + $expectedFiles = [ + 'index.html', + 'topics/index.html', + 'articles/index.html', + ]; + + $this->assertEqualsCanonicalizing($expectedFiles, array_keys($files)); + + $this->assertStringContainsString('

Page Title: Home

', $files['index.html']); + $this->assertStringContainsString('

Page Title: Topics

', $files['topics/index.html']); + $this->assertStringContainsString('

Articles Index Page Title

', $files['articles/index.html']); + } + /** @test */ public function it_generates_pages_to_custom_destination() { @@ -256,4 +280,78 @@ public function it_generates_pagination_with_custom_page_name_and_route() $this->assertStringContainsString('Total Pages: 3', $index); $this->assertStringContainsString('Prev Link: /articles/p-2', $index); } + + /** @test */ + public function it_generates_associated_paginated_pages_when_generating_only_urls_with_pagination() + { + $this->files->put(resource_path('views/articles/index.antlers.html'), <<<'EOT' +{{ collection:articles sort="date:asc" paginate="3" as="articles" }} + {{ articles }} + {{ title }} + {{ /articles }} + + {{ paginate }} + Current Page: {{ current_page }} + Total Pages: {{ total_pages }} + Prev Link: {{ prev_page }} + Next Link: {{ next_page }} + {{ /paginate }} +{{ /collection:articles }} +EOT + ); + + $this + ->partialMock(Filesystem::class) + ->shouldReceive('deleteDirectory') + ->with(config('statamic.ssg.destination'), true) + ->never(); + + $files = $this->generate(['urls' => ['articles']]); + + $expectedArticlesFiles = [ + 'articles/index.html', + 'articles/page/1/index.html', + 'articles/page/2/index.html', + 'articles/page/3/index.html', + ]; + + $this->assertEqualsCanonicalizing($expectedArticlesFiles, array_keys($files)); + + // Index assertions on implicit page 1 + $index = $files['articles/index.html']; + $this->assertStringContainsStrings(['One', 'Two', 'Three'], $index); + $this->assertStringNotContainsStrings(['Four', 'Five', 'Six'], $index); + $this->assertStringNotContainsStrings(['Seven', 'Eight'], $index); + $this->assertStringContainsString('Current Page: 1', $index); + $this->assertStringContainsString('Total Pages: 3', $index); + $this->assertStringContainsString('Next Link: /articles/page/2', $index); + + // Index assertions on explicit page 1 + $index = $files['articles/page/1/index.html']; + $this->assertStringContainsStrings(['One', 'Two', 'Three'], $index); + $this->assertStringNotContainsStrings(['Four', 'Five', 'Six'], $index); + $this->assertStringNotContainsStrings(['Seven', 'Eight'], $index); + $this->assertStringContainsString('Current Page: 1', $index); + $this->assertStringContainsString('Total Pages: 3', $index); + $this->assertStringContainsString('Next Link: /articles/page/2', $index); + + // Index assertions on page 2 + $index = $files['articles/page/2/index.html']; + $this->assertStringNotContainsStrings(['One', 'Two', 'Three'], $index); + $this->assertStringContainsStrings(['Four', 'Five', 'Six'], $index); + $this->assertStringNotContainsStrings(['Seven', 'Eight'], $index); + $this->assertStringContainsString('Current Page: 2', $index); + $this->assertStringContainsString('Total Pages: 3', $index); + $this->assertStringContainsString('Prev Link: /articles/page/1', $index); + $this->assertStringContainsString('Next Link: /articles/page/3', $index); + + // Index assertions on page 3 + $index = $files['articles/page/3/index.html']; + $this->assertStringNotContainsStrings(['One', 'Two', 'Three'], $index); + $this->assertStringNotContainsStrings(['Four', 'Five', 'Six'], $index); + $this->assertStringContainsStrings(['Seven', 'Eight'], $index); + $this->assertStringContainsString('Current Page: 3', $index); + $this->assertStringContainsString('Total Pages: 3', $index); + $this->assertStringContainsString('Prev Link: /articles/page/2', $index); + } } diff --git a/tests/Localized/GenerateTest.php b/tests/Localized/GenerateTest.php index b689542..7a2bff9 100644 --- a/tests/Localized/GenerateTest.php +++ b/tests/Localized/GenerateTest.php @@ -2,6 +2,7 @@ namespace Tests\Localized; +use Illuminate\Filesystem\Filesystem; use Statamic\Facades\Config; use Tests\Concerns\RunsGeneratorCommand; use Tests\TestCase; @@ -238,4 +239,64 @@ public function it_generates_localized_pagination_with_custom_page_name_and_rout $this->assertStringContainsString('Total Pages: 2', $index); $this->assertStringContainsString('Prev Link: /fr/le-articles/p-1', $index); } + + /** @test */ + public function it_generates_associated_paginated_pages_when_generating_only_localized_urls_with_pagination() + { + $this->files->put(resource_path('views/articles/index.antlers.html'), <<<'EOT' +{{ collection:articles sort="date:asc" paginate="3" as="articles" }} + {{ articles }} + {{ title }} + {{ /articles }} + + {{ paginate }} + Current Page: {{ current_page }} + Total Pages: {{ total_pages }} + Prev Link: {{ prev_page }} + Next Link: {{ next_page }} + {{ /paginate }} +{{ /collection:articles }} +EOT + ); + + $this + ->partialMock(Filesystem::class) + ->shouldReceive('deleteDirectory') + ->with(config('statamic.ssg.destination'), true) + ->never(); + + $files = $this->generate(['urls' => ['fr/le-articles']]); + + $expectedArticlesFiles = [ + 'fr/le-articles/index.html', + 'fr/le-articles/page/1/index.html', + 'fr/le-articles/page/2/index.html', + ]; + + $this->assertEqualsCanonicalizing($expectedArticlesFiles, array_keys($files)); + + // Index assertions on implicit page 1 + $index = $files['fr/le-articles/index.html']; + $this->assertStringContainsStrings(['Le One', 'Le Two', 'Le Three'], $index); + $this->assertStringNotContainsStrings(['Le Four', 'Le Five'], $index); + $this->assertStringContainsString('Current Page: 1', $index); + $this->assertStringContainsString('Total Pages: 2', $index); + $this->assertStringContainsString('Next Link: /fr/le-articles/page/2', $index); + + // Index assertions on explicit page 1 + $index = $files['fr/le-articles/page/1/index.html']; + $this->assertStringContainsStrings(['Le One', 'Le Two', 'Le Three'], $index); + $this->assertStringNotContainsStrings(['Le Four', 'Le Five'], $index); + $this->assertStringContainsString('Current Page: 1', $index); + $this->assertStringContainsString('Total Pages: 2', $index); + $this->assertStringContainsString('Next Link: /fr/le-articles/page/2', $index); + + // Index assertions on page 2 + $index = $files['fr/le-articles/page/2/index.html']; + $this->assertStringNotContainsStrings(['Le One', 'Le Two', 'Le Three'], $index); + $this->assertStringContainsStrings(['Le Four', 'Le Five'], $index); + $this->assertStringContainsString('Current Page: 2', $index); + $this->assertStringContainsString('Total Pages: 2', $index); + $this->assertStringContainsString('Prev Link: /fr/le-articles/page/1', $index); + } }