diff --git a/.gitignore b/.gitignore
index 5afbaf36..8ff296eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ composer.lock
/vendor
.phpunit.result.cache
.phpunit.cache
+.idea/
diff --git a/composer.json b/composer.json
index 547dc0a7..2e157103 100644
--- a/composer.json
+++ b/composer.json
@@ -26,7 +26,8 @@
},
"require-dev": {
"orchestra/testbench": "^8.0 || ^9.0",
- "phpunit/phpunit": "^10.0"
+ "phpunit/phpunit": "^10.0",
+ "laravel/pint": "^1.17"
},
"config": {
"allow-plugins": {
diff --git a/config/seo-pro.php b/config/seo-pro.php
index fc443d03..42161fce 100644
--- a/config/seo-pro.php
+++ b/config/seo-pro.php
@@ -22,6 +22,11 @@
'enabled' => true,
'url' => 'sitemap.xml',
'expire' => 60,
+ 'pagination' => [
+ 'enabled' => false,
+ 'url' => 'sitemap_{page}.xml',
+ 'limit' => 100,
+ ],
],
'humans' => [
diff --git a/resources/views/generated/sitemap_index.antlers.html b/resources/views/generated/sitemap_index.antlers.html
new file mode 100644
index 00000000..205c47eb
--- /dev/null
+++ b/resources/views/generated/sitemap_index.antlers.html
@@ -0,0 +1,8 @@
+{{ xml_header }}
+
+{{ sitemaps }}
+
+ {{ url }}
+
+{{ /sitemaps }}
+
diff --git a/routes/web.php b/routes/web.php
index bbdbb345..6a9515e1 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -2,5 +2,6 @@
use Statamic\SeoPro\Http\Controllers;
-Route::get(config('statamic.seo-pro.sitemap.url'), [Controllers\SitemapController::class, 'show']);
+Route::get(config('statamic.seo-pro.sitemap.url'), [Controllers\SitemapController::class, 'index']);
+Route::get(config('statamic.seo-pro.sitemap.pagination.url'), [Controllers\SitemapController::class, 'show'])->name('statamic.seo-pro.sitemap.page.show');
Route::get(config('statamic.seo-pro.humans.url'), [Controllers\HumansController::class, 'show']);
diff --git a/src/Http/Controllers/SitemapController.php b/src/Http/Controllers/SitemapController.php
index 90caf6e6..3baa51de 100755
--- a/src/Http/Controllers/SitemapController.php
+++ b/src/Http/Controllers/SitemapController.php
@@ -9,16 +9,47 @@
class SitemapController extends Controller
{
- public function show()
+ public function index()
{
abort_unless(config('statamic.seo-pro.sitemap.enabled'), 404);
$cacheUntil = Carbon::now()->addMinutes(config('statamic.seo-pro.sitemap.expire'));
- $content = Cache::remember(Sitemap::CACHE_KEY, $cacheUntil, function () {
+ if (config('statamic.seo-pro.sitemap.pagination.enabled', false)) {
+ $content = Cache::remember(Sitemap::CACHE_KEY.'_index', $cacheUntil, function () {
+ return view('seo-pro::sitemap_index', [
+ 'xml_header' => '',
+ 'sitemaps' => Sitemap::paginatedSitemaps(),
+ ])->render();
+ });
+ } else {
+ $content = Cache::remember(Sitemap::CACHE_KEY, $cacheUntil, function () {
+ return view('seo-pro::sitemap', [
+ 'xml_header' => '',
+ 'pages' => Sitemap::pages(),
+ ])->render();
+ });
+ }
+
+ return response($content)->header('Content-Type', 'text/xml');
+ }
+
+ public function show($page)
+ {
+ abort_unless(config('statamic.seo-pro.sitemap.enabled'), 404);
+ abort_unless(config('statamic.seo-pro.sitemap.pagination.enabled'), 404);
+ abort_unless(filter_var($page, FILTER_VALIDATE_INT), 404);
+
+ $cacheUntil = Carbon::now()->addMinutes(config('statamic.seo-pro.sitemap.expire'));
+
+ $cacheKey = Sitemap::CACHE_KEY.'_'.$page;
+
+ $content = Cache::remember($cacheKey, $cacheUntil, function () use ($page) {
+ abort_if(empty($pages = Sitemap::paginatedPages($page)), 404);
+
return view('seo-pro::sitemap', [
- 'pages' => Sitemap::pages(),
'xml_header' => '',
+ 'pages' => $pages,
])->render();
});
diff --git a/src/Sitemap/Sitemap.php b/src/Sitemap/Sitemap.php
index 84d8b0c5..df6c55a8 100644
--- a/src/Sitemap/Sitemap.php
+++ b/src/Sitemap/Sitemap.php
@@ -4,6 +4,7 @@
use Statamic\Facades\Blink;
use Statamic\Facades\Collection;
+use Statamic\Facades\Entry;
use Statamic\Facades\Taxonomy;
use Statamic\SeoPro\Cascade;
use Statamic\SeoPro\GetsSectionDefaults;
@@ -31,6 +32,63 @@ public static function pages()
->toArray();
}
+ public static function paginatedPages(int $page)
+ {
+ $sitemap = new static;
+
+ $perPage = config('statamic.seo-pro.sitemap.pagination.limit', 100);
+ $offset = ($page - 1) * $perPage;
+ $remaining = $perPage;
+
+ $pages = collect([]);
+
+ $entryCount = $sitemap->publishedEntriesCount() - 1;
+
+ if ($offset < $entryCount) {
+ $entries = $sitemap->publishedEntries()->skip($offset)->take($perPage);
+
+ if ($entries->count() < $remaining) {
+ $remaining -= $entries->count();
+ }
+
+ $pages = $pages->merge($entries);
+ }
+
+ if ($remaining > 0) {
+ $offset = max($offset - $entryCount, 0);
+
+ $pages = $pages->merge(
+ collect($sitemap->publishedTerms())
+ ->merge($sitemap->publishedCollectionTerms())
+ ->skip($offset)
+ ->take($remaining)
+ );
+ }
+
+ if ($pages->isEmpty()) {
+ return [];
+ }
+
+ return $sitemap->getPages($pages)
+ ->values()
+ ->map
+ ->toArray();
+ }
+
+ public static function paginatedSitemaps()
+ {
+ $sitemap = new static;
+
+ // would be nice to make terms a count query rather than getting the count from the terms collection
+ $count = $sitemap->publishedEntriesCount() + $sitemap->publishedTerms()->count() + $sitemap->publishedCollectionTerms()->count();
+
+ $sitemapCount = ceil($count / config('statamic.seo-pro.sitemap.pagination.limit', 100));
+
+ return collect(range(1, $sitemapCount))
+ ->map(fn ($page) => ['url' => route('statamic.seo-pro.sitemap.page.show', ['page' => $page])])
+ ->all();
+ }
+
protected function getPages($items)
{
return $items
@@ -54,20 +112,33 @@ protected function getPages($items)
->filter();
}
- protected function publishedEntries()
+ private function publishedEntriesQuery()
{
- return Collection::all()
- ->flatMap(function ($collection) {
+ $collections = Collection::all()
+ ->map(function ($collection) {
return $collection->cascade('seo') !== false
- ? $collection->queryEntries()->get()
- : collect();
+ ? $collection->handle()
+ : false;
})
- ->filter(function ($entry) {
- return $entry->status() === 'published';
- })
- ->reject(function ($entry) {
- return is_null($entry->uri());
- });
+ ->filter()
+ ->values()
+ ->all();
+
+ return Entry::query()
+ ->whereIn('collection', $collections)
+ ->whereNotNull('uri')
+ ->whereStatus('published')
+ ->orderBy('uri');
+ }
+
+ protected function publishedEntries()
+ {
+ return $this->publishedEntriesQuery()->lazy();
+ }
+
+ protected function publishedEntriesCount()
+ {
+ return $this->publishedEntriesQuery()->count();
}
protected function publishedTerms()
diff --git a/tests/SitemapTest.php b/tests/SitemapTest.php
index 4eddc968..98df06ff 100644
--- a/tests/SitemapTest.php
+++ b/tests/SitemapTest.php
@@ -44,15 +44,15 @@ public function it_outputs_sitemap_xml()
- http://cool-runnings.com/magic
- $today
+ http://cool-runnings.com/about
+ 2020-01-17
monthly
0.5
- http://cool-runnings.com/nectar
- $today
+ http://cool-runnings.com/articles
+ 2020-01-17
monthly
0.5
@@ -65,15 +65,15 @@ public function it_outputs_sitemap_xml()
- http://cool-runnings.com/about
- 2020-01-17
+ http://cool-runnings.com/magic
+ $today
monthly
0.5
- http://cool-runnings.com/articles
- 2020-01-17
+ http://cool-runnings.com/nectar
+ $today
monthly
0.5
@@ -219,4 +219,150 @@ protected function getPagesFromSitemapXml($content)
return collect($data['url']);
}
+
+ /** @test */
+ public function it_outputs_paginated_sitemap_index_xml()
+ {
+ config()->set('statamic.seo-pro.sitemap.pagination.enabled', true);
+ config()->set('statamic.seo-pro.sitemap.pagination.limit', 5);
+
+ $content = $this
+ ->get('/sitemap.xml')
+ ->assertOk()
+ ->assertHeader('Content-Type', 'text/xml; charset=UTF-8')
+ ->getContent();
+
+ $expected = <<<'EOT'
+
+
+
+
+ http://cool-runnings.com/sitemap_1.xml
+
+
+
+ http://cool-runnings.com/sitemap_2.xml
+
+
+
+
+EOT;
+
+ $this->assertEquals($expected, $content);
+ }
+
+ /** @test */
+ public function it_outputs_paginated_sitemap_page_xml()
+ {
+ config()->set('statamic.seo-pro.sitemap.pagination.enabled', true);
+ config()->set('statamic.seo-pro.sitemap.pagination.limit', 5);
+
+ $content = $this
+ ->get('/sitemap_1.xml')
+ ->assertOk()
+ ->assertHeader('Content-Type', 'text/xml; charset=UTF-8')
+ ->getContent();
+
+ $this->assertCount(5, $this->getPagesFromSitemapXml($content));
+
+ $today = now()->format('Y-m-d');
+
+ $expected = <<<"EOT"
+
+
+
+
+ http://cool-runnings.com
+ 2020-11-24
+ monthly
+ 0.5
+
+
+
+ http://cool-runnings.com/about
+ 2020-01-17
+ monthly
+ 0.5
+
+
+
+ http://cool-runnings.com/articles
+ 2020-01-17
+ monthly
+ 0.5
+
+
+
+ http://cool-runnings.com/dance
+ $today
+ monthly
+ 0.5
+
+
+
+ http://cool-runnings.com/magic
+ $today
+ monthly
+ 0.5
+
+
+
+
+EOT;
+
+ $this->assertEquals($expected, $content);
+
+ $content = $this
+ ->get('/sitemap_2.xml')
+ ->assertOk()
+ ->assertHeader('Content-Type', 'text/xml; charset=UTF-8')
+ ->getContent();
+
+ $this->assertCount(2, $this->getPagesFromSitemapXml($content));
+
+ $today = now()->format('Y-m-d');
+
+ $expected = <<<"EOT"
+
+
+
+
+ http://cool-runnings.com/nectar
+ $today
+ monthly
+ 0.5
+
+
+
+ http://cool-runnings.com/topics
+ 2020-01-20
+ monthly
+ 0.5
+
+
+
+
+EOT;
+
+ $this->assertEquals($expected, $content);
+ }
+
+ /** @test */
+ public function it_404s_on_invalid_pagination_urls()
+ {
+ config()->set('statamic.seo-pro.sitemap.pagination.enabled', true);
+ config()->set('statamic.seo-pro.sitemap.pagination.limit', 5);
+
+ $this
+ ->get('/sitemap_3.xml')
+ ->assertNotFound();
+
+ $this
+ ->get('/sitemap_3a.xml')
+ ->assertNotFound();
+
+ $this
+ ->get('/sitemap_a.xml')
+ ->assertNotFound();
+ }
}