Skip to content

Commit

Permalink
Sitemap performance improvements (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
aerni authored Aug 9, 2024
1 parent a8d893d commit 8bb761e
Show file tree
Hide file tree
Showing 35 changed files with 493 additions and 217 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"require": {
"php": "^8.2",
"laravel/framework": "^10.0 || ^11.0",
"laravel/prompts": "^0.1.24",
"spatie/browsershot": "^4.0",
"spatie/image": "^3.4",
"spatie/laravel-ray": "^1.32",
Expand Down
17 changes: 14 additions & 3 deletions config/advanced-seo.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,25 @@

/*
|--------------------------------------------------------------------------
| Cache Expiry
| Storage Path
|--------------------------------------------------------------------------
|
| The sitemap cache expiry in minutes.
| The directory where your sitemap files will be located.
|
*/

'expiry' => 60,
'path' => storage_path('statamic/sitemaps'),

/*
|--------------------------------------------------------------------------
| Queue
|--------------------------------------------------------------------------
|
| The queue that is used when generating the sitemaps.
|
*/

'queue' => 'default',

],

Expand Down
2 changes: 1 addition & 1 deletion routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@

Route::name('advanced-seo.')->group(function () {
Route::get('/sitemap.xml', [SitemapController::class, 'index'])->name('sitemap.index');
Route::get('/sitemap-{type}-{handle}.xml', [SitemapController::class, 'show'])->name('sitemap.show');
Route::get('/sitemaps/{id}.xml', [SitemapController::class, 'show'])->name('sitemap.show');
Route::get('/sitemap.xsl', [SitemapController::class, 'xsl'])->name('sitemap.xsl');
});
45 changes: 30 additions & 15 deletions src/Actions/IncludeInSitemap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use Aerni\AdvancedSeo\Concerns\AsAction;
use Aerni\AdvancedSeo\Concerns\EvaluatesIndexability;
use Aerni\AdvancedSeo\Facades\Seo;
use Aerni\AdvancedSeo\Data\DefaultsData;
use Aerni\AdvancedSeo\Features\Sitemap;
use Statamic\Contracts\Entries\Collection;
use Statamic\Contracts\Entries\Entry;
use Statamic\Contracts\Taxonomies\Taxonomy;
use Statamic\Contracts\Taxonomies\Term;
Expand All @@ -15,37 +17,50 @@ class IncludeInSitemap
use AsAction;
use EvaluatesIndexability;

public function handle(Entry|Term|Taxonomy $model, ?string $locale = null): bool
protected DefaultsData $data;

public function handle(Entry|Term|Collection|Taxonomy $model, ?string $locale = null): bool
{
$locale ??= $model->locale();
if ($this->localeIsRequired($model) && is_null($locale)) {
throw new \Exception('A locale is required if the model is a Collection or Taxonomy.');
}

$this->data = GetDefaultsData::handle($model);

if ($locale) {
$this->data->locale = $locale;
}

return Blink::once("{$model->id()}::{$locale}", fn () => match (true) {
return Blink::once("include-in-sitemap-{$this->data->id()}::{$model->id()}", fn () => match (true) {
$model instanceof Entry => $this->includeEntryOrTermInSitemap($model),
$model instanceof Term => $this->includeEntryOrTermInSitemap($model),
$model instanceof Taxonomy => $this->includeTaxonomyInSitemap($model, $locale)
$model instanceof Collection => $this->includeCollectionOrTaxonomyInSitemap($model),
$model instanceof Taxonomy => $this->includeCollectionOrTaxonomyInSitemap($model)
});
}

protected function includeEntryOrTermInSitemap(Entry|Term $model): bool
{
return ! $this->isExcludedFromSitemap($model, $model->locale)
return Sitemap::enabled($this->data)
&& $this->isIndexableEntryOrTerm($model)
&& $model->seo_sitemap_enabled
&& $model->seo_canonical_type == 'current';
}

protected function includeTaxonomyInSitemap(Taxonomy $taxonomy, string $locale): bool
protected function includeCollectionOrTaxonomyInSitemap(Collection|Taxonomy $model): bool
{
return ! $this->isExcludedFromSitemap($taxonomy, $locale)
&& $this->isIndexableSite($locale);
// TODO: Currently, taxonomies don't have a routes method. But they should once PR https://github.com/statamic/cms/pull/8627 is merged.
$hasRouteForSite = $model instanceof Collection
? $model->routes()->filter()->has($this->data->locale)
: true;

return Sitemap::enabled($this->data)
&& $this->isIndexableSite($this->data->locale)
&& $hasRouteForSite;
}

protected function isExcludedFromSitemap(Entry|Term|Taxonomy $model, string $locale): bool
protected function localeIsRequired($model): bool
{
$excluded = Seo::find('site', 'indexing')
?->in($locale)
?->value('excluded_'.EvaluateModelType::handle($model)) ?? [];

return in_array(EvaluateModelHandle::handle($model), $excluded);
return $model instanceof Taxonomy || $model instanceof Collection;
}
}
2 changes: 1 addition & 1 deletion src/Blueprints/BaseBlueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ abstract class BaseBlueprint implements Contract

public static function make(): self
{
return new static();
return new static;
}

public function data(DefaultsData $data): self
Expand Down
48 changes: 48 additions & 0 deletions src/Commands/GenerateSitemaps.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Aerni\AdvancedSeo\Commands;

use Aerni\AdvancedSeo\Facades\Sitemap;
use Aerni\AdvancedSeo\Jobs\GenerateSitemapsJob;
use Illuminate\Console\Command;
use Statamic\Console\RunsInPlease;

use function Laravel\Prompts\error;
use function Laravel\Prompts\info;
use function Laravel\Prompts\spin;
use function Laravel\Prompts\warning;

class GenerateSitemaps extends Command
{
use RunsInPlease;

protected $signature = 'seo:generate-sitemaps {--queue}';

protected $description = 'Generate the sitemaps';

protected bool $shouldQueue = false;

public function handle()
{
if (! config('advanced-seo.sitemap.enabled')) {
return error('The sitemap feature is disabled. You need to enable it to generate the sitemaps.');
}

$this->shouldQueue = $this->option('queue');

if ($this->shouldQueue && config('queue.default') === 'sync') {
warning('The queue connection is set to "sync". Queueing will be disabled.');
$this->shouldQueue = false;
}

$sitemaps = collect([Sitemap::index()])->merge(Sitemap::all());

$this->shouldQueue
? GenerateSitemapsJob::dispatch($sitemaps)
: spin(fn () => GenerateSitemapsJob::dispatchSync($sitemaps), 'Generating sitemaps ...');

$this->shouldQueue
? info('All requests to generate the sitemaps have been added to the queue.')
: info('The sitemaps have been succesfully generated.');
}
}
2 changes: 0 additions & 2 deletions src/Contracts/Sitemap.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ public function id(): string;
public function url(): string;

public function lastmod(): ?string;

public function clearCache(): void;
}
14 changes: 14 additions & 0 deletions src/Contracts/SitemapFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Aerni\AdvancedSeo\Contracts;

interface SitemapFile
{
public function filename(): string;

public function file(): ?string;

public function path(): string;

public function save(): self;
}
12 changes: 12 additions & 0 deletions src/Contracts/SitemapIndex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Aerni\AdvancedSeo\Contracts;

use Illuminate\Support\Collection;

interface SitemapIndex
{
public function add(Sitemap $sitemap): self;

public function sitemaps(): Collection;
}
2 changes: 0 additions & 2 deletions src/Contracts/SitemapUrl.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,4 @@ public function changefreq(): string|self|null;
public function priority(): string|self|null;

public function site(): string|self;

public function canonicalTypeIsCurrent(): bool;
}
2 changes: 1 addition & 1 deletion src/Fields/BaseFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ abstract class BaseFields implements Fields

public static function make(): self
{
return new static();
return new static;
}

public function data(DefaultsData $data): self
Expand Down
35 changes: 18 additions & 17 deletions src/GraphQL/Fields/SitemapField.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,23 @@ public function type(): Type

public function resolve($root, $args, $context, ResolveInfo $info): ?array
{
$sitemaps = Sitemap::{"{$info->fieldName}Sitemaps"}();

if ($baseUrl = $args['baseUrl'] ?? null) {
$sitemaps = $sitemaps->each(fn ($sitemap) => $sitemap->baseUrl($baseUrl));
}

if ($handle = $args['handle'] ?? null) {
$sitemaps = $sitemaps->filter(fn ($sitemap) => $handle === $sitemap->handle());
}

$sitemapUrls = $sitemaps->flatMap->urls();

if ($site = $args['site'] ?? null) {
$sitemapUrls = $sitemapUrls->filter(fn ($url) => $url->site() === $site);
}

return $sitemapUrls->isNotEmpty() ? $sitemapUrls->toArray() : null;
$sitemaps = Sitemap::all()
->filter(fn ($sitemap) => $sitemap->type() === $info->fieldName)
->when(
$handle = data_get($args, 'handle'),
fn ($sitemaps) => $sitemaps->filter(fn ($sitemap) => $sitemap->handle() === $handle)
);

$urls = $sitemaps->flatMap->urls()
->when(
$site = data_get($args, 'site'),
fn ($urls) => $urls->filter(fn ($url) => $url->site() === $site)
)
->when(
$baseUrl = data_get($args, 'baseUrl'),
fn ($urls) => $urls->each(fn ($url) => $url->baseUrl($baseUrl))
);

return $urls->isNotEmpty() ? $urls->toArray() : null;
}
}
2 changes: 1 addition & 1 deletion src/Http/Controllers/Cp/SeoDefaultsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ protected function flashDefaultsUnavailable(): void
'type' => Str::singular($this->type()),
]));

throw new NotFoundHttpException();
throw new NotFoundHttpException;
}

protected function redirectToIndex(SeoDefaultSet $set, string $site): RedirectResponse
Expand Down
67 changes: 21 additions & 46 deletions src/Http/Controllers/Web/SitemapController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,42 @@

namespace Aerni\AdvancedSeo\Http\Controllers\Web;

use Aerni\AdvancedSeo\Facades\Sitemap;
use Aerni\AdvancedSeo\Concerns\EvaluatesIndexability;
use Aerni\AdvancedSeo\Contracts\Sitemap;
use Aerni\AdvancedSeo\Contracts\SitemapIndex;
use Aerni\AdvancedSeo\Facades\Sitemap as SitemapRepository;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Cache;
use Statamic\Exceptions\NotFoundHttpException;
use Statamic\Facades\Addon;

class SitemapController extends Controller
{
public function index(): Response
use EvaluatesIndexability;

public function __construct()
{
throw_unless(config('advanced-seo.sitemap.enabled'), new NotFoundHttpException);

$sitemaps = Cache::remember('advanced-seo::sitemaps::index', Sitemap::cacheExpiry(), function () {
return Sitemap::all()
->filter(fn ($sitemap) => $sitemap->urls()->isNotEmpty())
->toArray();
});

throw_unless($sitemaps, new NotFoundHttpException);

return response()
->view('advanced-seo::sitemaps.index', [
'sitemaps' => $sitemaps,
'version' => Addon::get('aerni/advanced-seo')->version(),
])
->header('Content-Type', 'text/xml')
->header('X-Robots-Tag', 'noindex, nofollow');
throw_unless($this->crawlingIsEnabled(), new NotFoundHttpException);
}

public function show(string $type, string $handle): Response
public function index(): SitemapIndex
{
throw_unless(config('advanced-seo.sitemap.enabled'), new NotFoundHttpException);

$id = "{$type}::{$handle}";

$urls = Cache::remember(
"advanced-seo::sitemaps::{$id}",
Sitemap::cacheExpiry(),
fn () => Sitemap::find($id)?->urls()->toArray()
);

throw_unless($urls, new NotFoundHttpException);
return SitemapRepository::index();
}

return response()
->view('advanced-seo::sitemaps.show', [
'urls' => $urls,
'version' => Addon::get('aerni/advanced-seo')->version(),
])
->header('Content-Type', 'text/xml')
->header('X-Robots-Tag', 'noindex, nofollow');
public function show(string $id): Sitemap
{
return throw_unless(SitemapRepository::find($id), NotFoundHttpException::class);
}

public function xsl(): Response
{
throw_unless(config('advanced-seo.sitemap.enabled'), new NotFoundHttpException);

$path = __DIR__.'/../../../../resources/views/sitemaps/sitemap.xsl';

return response(file_get_contents($path))
->header('Content-Type', 'text/xsl')
->header('X-Robots-Tag', 'noindex, nofollow');
return response(
content: SitemapRepository::xsl(),
headers: [
'Content-Type' => 'text/xsl',
'X-Robots-Tag' => 'noindex, nofollow',
],
);
}
}
2 changes: 1 addition & 1 deletion src/Http/Controllers/Web/SocialImagesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function show(string $theme, string $type, string $id): Response
throw_unless($data = $this->getData($id), new NotFoundHttpException);

// Throw if the data is not an entry or term.
throw_unless($data instanceof Entry || $data instanceof LocalizedTerm, new NotFoundHttpException());
throw_unless($data instanceof Entry || $data instanceof LocalizedTerm, new NotFoundHttpException);

// Throw if the social image type is not supported.
throw_unless($model = SocialImage::findModel(Str::replace('-', '_', $type)), new NotFoundHttpException);
Expand Down
Loading

0 comments on commit 8bb761e

Please sign in to comment.