From a8d893d76fd9ffd5cb1c800035289f00bc5cb0fc Mon Sep 17 00:00:00 2001 From: Michael Aerni Date: Sat, 20 Jul 2024 16:54:50 -0400 Subject: [PATCH] Improve sitemaps (#165) --- resources/views/sitemaps/index.blade.php | 14 +- resources/{xsl => views/sitemaps}/sitemap.xsl | 0 src/Actions/IncludeInSitemap.php | 51 +++++++ src/Actions/Indexable.php | 109 --------------- src/Concerns/AsAction.php | 25 ++++ src/Concerns/EvaluatesContextType.php | 27 ++++ .../Concerns/EvaluatesIndexability.php | 15 +-- src/Contracts/SitemapUrl.php | 4 +- src/Facades/Sitemap.php | 2 +- src/GraphQL/Fields/SitemapField.php | 13 +- .../Controllers/Web/SitemapController.php | 57 ++++---- src/Sitemap/CollectionSitemap.php | 29 ---- src/Sitemap/SitemapIndex.php | 42 ------ src/Sitemap/SitemapRepository.php | 44 ------ src/Sitemap/TaxonomySitemap.php | 127 ------------------ src/{Sitemap => Sitemaps}/BaseSitemap.php | 28 +++- src/{Sitemap => Sitemaps}/BaseSitemapUrl.php | 18 +-- .../Collections/CollectionSitemap.php | 32 +++++ .../Collections/EntrySitemapUrl.php} | 25 ++-- .../Custom}/CustomSitemap.php | 7 +- .../Custom}/CustomSitemapUrl.php | 3 +- src/Sitemaps/SitemapRepository.php | 86 ++++++++++++ .../CollectionTaxonomySitemapUrl.php | 25 ++-- .../Taxonomies}/CollectionTermSitemapUrl.php | 23 ++-- src/Sitemaps/Taxonomies/TaxonomySitemap.php | 115 ++++++++++++++++ .../Taxonomies}/TaxonomySitemapUrl.php | 41 ++---- .../Taxonomies}/TermSitemapUrl.php | 28 ++-- src/View/Concerns/EvaluatesContextType.php | 26 ---- src/View/Concerns/HasHreflang.php | 9 +- src/View/GraphQlCascade.php | 2 +- src/View/ViewCascade.php | 10 +- 31 files changed, 490 insertions(+), 547 deletions(-) rename resources/{xsl => views/sitemaps}/sitemap.xsl (100%) create mode 100644 src/Actions/IncludeInSitemap.php delete mode 100644 src/Actions/Indexable.php create mode 100644 src/Concerns/AsAction.php create mode 100644 src/Concerns/EvaluatesContextType.php rename src/{View => }/Concerns/EvaluatesIndexability.php (83%) delete mode 100644 src/Sitemap/CollectionSitemap.php delete mode 100644 src/Sitemap/SitemapIndex.php delete mode 100644 src/Sitemap/SitemapRepository.php delete mode 100644 src/Sitemap/TaxonomySitemap.php rename src/{Sitemap => Sitemaps}/BaseSitemap.php (63%) rename src/{Sitemap => Sitemaps}/BaseSitemapUrl.php (65%) create mode 100644 src/Sitemaps/Collections/CollectionSitemap.php rename src/{Sitemap/CollectionSitemapUrl.php => Sitemaps/Collections/EntrySitemapUrl.php} (74%) rename src/{Sitemap => Sitemaps/Custom}/CustomSitemap.php (76%) rename src/{Sitemap => Sitemaps/Custom}/CustomSitemapUrl.php (97%) create mode 100644 src/Sitemaps/SitemapRepository.php rename src/{Sitemap => Sitemaps/Taxonomies}/CollectionTaxonomySitemapUrl.php (72%) rename src/{Sitemap => Sitemaps/Taxonomies}/CollectionTermSitemapUrl.php (73%) create mode 100644 src/Sitemaps/Taxonomies/TaxonomySitemap.php rename src/{Sitemap => Sitemaps/Taxonomies}/TaxonomySitemapUrl.php (70%) rename src/{Sitemap => Sitemaps/Taxonomies}/TermSitemapUrl.php (68%) delete mode 100644 src/View/Concerns/EvaluatesContextType.php diff --git a/resources/views/sitemaps/index.blade.php b/resources/views/sitemaps/index.blade.php index aeba23fe..94b04120 100644 --- a/resources/views/sitemaps/index.blade.php +++ b/resources/views/sitemaps/index.blade.php @@ -5,14 +5,12 @@ @foreach ($sitemaps as $sitemap) - @if ($sitemap->urls()->count() >= 1) - - {{ $sitemap->url() }} + + {{ $sitemap['url'] }} - @if($sitemap->lastmod()) - {{ $sitemap->lastmod() }} - @endif - - @endif + @isset($sitemap['lastmod']) + {{ $sitemap['lastmod'] }} + @endisset + @endforeach diff --git a/resources/xsl/sitemap.xsl b/resources/views/sitemaps/sitemap.xsl similarity index 100% rename from resources/xsl/sitemap.xsl rename to resources/views/sitemaps/sitemap.xsl diff --git a/src/Actions/IncludeInSitemap.php b/src/Actions/IncludeInSitemap.php new file mode 100644 index 00000000..7957aea3 --- /dev/null +++ b/src/Actions/IncludeInSitemap.php @@ -0,0 +1,51 @@ +locale(); + + return Blink::once("{$model->id()}::{$locale}", fn () => match (true) { + $model instanceof Entry => $this->includeEntryOrTermInSitemap($model), + $model instanceof Term => $this->includeEntryOrTermInSitemap($model), + $model instanceof Taxonomy => $this->includeTaxonomyInSitemap($model, $locale) + }); + } + + protected function includeEntryOrTermInSitemap(Entry|Term $model): bool + { + return ! $this->isExcludedFromSitemap($model, $model->locale) + && $this->isIndexableEntryOrTerm($model) + && $model->seo_sitemap_enabled + && $model->seo_canonical_type == 'current'; + } + + protected function includeTaxonomyInSitemap(Taxonomy $taxonomy, string $locale): bool + { + return ! $this->isExcludedFromSitemap($taxonomy, $locale) + && $this->isIndexableSite($locale); + } + + protected function isExcludedFromSitemap(Entry|Term|Taxonomy $model, string $locale): bool + { + $excluded = Seo::find('site', 'indexing') + ?->in($locale) + ?->value('excluded_'.EvaluateModelType::handle($model)) ?? []; + + return in_array(EvaluateModelHandle::handle($model), $excluded); + } +} diff --git a/src/Actions/Indexable.php b/src/Actions/Indexable.php deleted file mode 100644 index a8017ad7..00000000 --- a/src/Actions/Indexable.php +++ /dev/null @@ -1,109 +0,0 @@ -crawlingIsEnabled()) { - return false; - } - - $locale = $locale ?? $model->locale(); - - return Blink::once("{$model->id()}::{$locale}", function () use ($model, $locale) { - return match (true) { - ($model instanceof Entry) => self::isIndexableEntry($model), - ($model instanceof Term) => self::isIndexableTerm($model), - ($model instanceof Taxonomy) => self::isIndexableTaxonomy($model, $locale), - 'default' => true, - }; - }); - } - - protected static function isIndexableEntry(Entry $entry): bool - { - return self::modelIsIndexable($entry, $entry->locale) && self::contentIsIndexable($entry); - } - - protected static function isIndexableTerm(Term $term): bool - { - return self::modelIsIndexable($term, $term->locale) && self::contentIsIndexable($term); - } - - protected static function isIndexableTaxonomy(Taxonomy $taxonomy, string $locale): bool - { - return self::modelIsIndexable($taxonomy, $locale); - } - - protected static function modelIsIndexable(Entry|Term|Taxonomy $model, string $locale): bool - { - $type = EvaluateModelType::handle($model); - $handle = EvaluateModelHandle::handle($model); - - $disabled = config("advanced-seo.disabled.{$type}", []); - - // Check if the collection/taxonomy is set to be disabled globally. - if (in_array($handle, $disabled)) { - return false; - } - - $config = Seo::find('site', 'indexing')?->in($locale); - - // If there is no config, the sitemap should be indexable. - if (is_null($config)) { - return true; - } - - // If we have a global noindex, the sitemap shouldn't be indexable. - if ($config->value('noindex')) { - return false; - } - - // Check if the collection/taxonomy is set to be excluded from the sitemap - $excluded = $config->value("excluded_{$type}") ?? []; - - // If the collection/taxonomy is excluded, the sitemap shouldn't be indexable. - return ! in_array($handle, $excluded); - } - - protected static function contentIsIndexable(Entry|Term $model): bool - { - // Don't index models that are not published. - if ($model->published() === false) { - return false; - } - - // Don't index models that have no URI. - if ($model->uri() === null) { - return false; - } - - // If the sitemap is disabled, we shouldn't index the model. - if (! $model->seo_sitemap_enabled) { - return false; - } - - // Check if noindex is enabled. - if ($model->seo_noindex) { - return false; - } - - // If the canonical type isn't current, we shouldn't index the model. - if ($model->seo_canonical_type != 'current') { - return false; - } - - return true; - } -} diff --git a/src/Concerns/AsAction.php b/src/Concerns/AsAction.php new file mode 100644 index 00000000..c47e79a0 --- /dev/null +++ b/src/Concerns/AsAction.php @@ -0,0 +1,25 @@ +handle(...$arguments); + } +} diff --git a/src/Concerns/EvaluatesContextType.php b/src/Concerns/EvaluatesContextType.php new file mode 100644 index 00000000..27112229 --- /dev/null +++ b/src/Concerns/EvaluatesContextType.php @@ -0,0 +1,27 @@ +value('is_entry') || $context->value('is_term'); + } + + protected function contextIsTaxonomy(Context $context): bool + { + return $context->get('page') instanceof Taxonomy + && $context->get('page')->collection() === null; + } + + protected function contextIsCollectionTaxonomy(Context $context): bool + { + return $context->get('page') instanceof Taxonomy + && $context->get('page')->collection() instanceof Collection; + } +} diff --git a/src/View/Concerns/EvaluatesIndexability.php b/src/Concerns/EvaluatesIndexability.php similarity index 83% rename from src/View/Concerns/EvaluatesIndexability.php rename to src/Concerns/EvaluatesIndexability.php index 09672771..626cb36e 100644 --- a/src/View/Concerns/EvaluatesIndexability.php +++ b/src/Concerns/EvaluatesIndexability.php @@ -1,6 +1,6 @@ contextIsEntryOrTerm() + $model = $this->contextIsEntryOrTerm($context) ? $context->get('id')->augmentable() : $context->get('site'); @@ -37,13 +39,10 @@ protected function isIndexableEntryOrTerm(Entry|LocalizedTerm $model): bool && ! $model->seo_noindex; // Models with noindex should not be indexed. } - protected function isIndexableSite(string $locale): bool + protected function isIndexableSite(string $site): bool { - if (! $this->crawlingIsEnabled()) { - return false; - } - - return ! Seo::find('site', 'indexing')?->in($locale)?->noindex; + return $this->crawlingIsEnabled() + && ! Seo::find('site', 'indexing')?->in($site)?->noindex; } protected function crawlingIsEnabled(): bool diff --git a/src/Contracts/SitemapUrl.php b/src/Contracts/SitemapUrl.php index 9cfb32a1..99d621b4 100644 --- a/src/Contracts/SitemapUrl.php +++ b/src/Contracts/SitemapUrl.php @@ -16,7 +16,5 @@ public function priority(): string|self|null; public function site(): string|self; - public function isCanonicalUrl(): bool; - - public function toArray(): ?array; + public function canonicalTypeIsCurrent(): bool; } diff --git a/src/Facades/Sitemap.php b/src/Facades/Sitemap.php index 0c72117d..5aafdc93 100644 --- a/src/Facades/Sitemap.php +++ b/src/Facades/Sitemap.php @@ -8,6 +8,6 @@ class Sitemap extends Facade { protected static function getFacadeAccessor() { - return \Aerni\AdvancedSeo\Sitemap\SitemapRepository::class; + return \Aerni\AdvancedSeo\Sitemaps\SitemapRepository::class; } } diff --git a/src/GraphQL/Fields/SitemapField.php b/src/GraphQL/Fields/SitemapField.php index 7a69674b..ab809205 100644 --- a/src/GraphQL/Fields/SitemapField.php +++ b/src/GraphQL/Fields/SitemapField.php @@ -2,11 +2,10 @@ namespace Aerni\AdvancedSeo\GraphQL\Fields; +use Aerni\AdvancedSeo\Facades\Sitemap; use Aerni\AdvancedSeo\GraphQL\Types\SeoSitemapType; -use Aerni\AdvancedSeo\Sitemap\SitemapIndex; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; -use Illuminate\Support\Collection; use Rebing\GraphQL\Support\Field; use Statamic\Facades\GraphQL; @@ -37,12 +36,12 @@ public function args(): array public function type(): Type { - return GraphQl::listOf(GraphQL::type(SeoSitemapType::NAME)); + return GraphQL::listOf(GraphQL::type(SeoSitemapType::NAME)); } - public function resolve($root, $args, $context, ResolveInfo $info): ?Collection + public function resolve($root, $args, $context, ResolveInfo $info): ?array { - $sitemaps = (new SitemapIndex)->{"{$info->fieldName}Sitemaps"}(); + $sitemaps = Sitemap::{"{$info->fieldName}Sitemaps"}(); if ($baseUrl = $args['baseUrl'] ?? null) { $sitemaps = $sitemaps->each(fn ($sitemap) => $sitemap->baseUrl($baseUrl)); @@ -55,9 +54,9 @@ public function resolve($root, $args, $context, ResolveInfo $info): ?Collection $sitemapUrls = $sitemaps->flatMap->urls(); if ($site = $args['site'] ?? null) { - $sitemapUrls = $sitemapUrls->where('site', $site); + $sitemapUrls = $sitemapUrls->filter(fn ($url) => $url->site() === $site); } - return $sitemapUrls->isNotEmpty() ? $sitemapUrls : null; + return $sitemapUrls->isNotEmpty() ? $sitemapUrls->toArray() : null; } } diff --git a/src/Http/Controllers/Web/SitemapController.php b/src/Http/Controllers/Web/SitemapController.php index 16d9e0c3..0a051e1e 100644 --- a/src/Http/Controllers/Web/SitemapController.php +++ b/src/Http/Controllers/Web/SitemapController.php @@ -15,49 +15,54 @@ public function index(): Response { throw_unless(config('advanced-seo.sitemap.enabled'), new NotFoundHttpException); - $view = Cache::remember('advanced-seo::sitemaps::index', Sitemap::cacheExpiry(), function () { - return view('advanced-seo::sitemaps.index', [ - 'sitemaps' => Sitemap::all(), - 'version' => Addon::get('aerni/advanced-seo')->version(), - ])->render(); + $sitemaps = Cache::remember('advanced-seo::sitemaps::index', Sitemap::cacheExpiry(), function () { + return Sitemap::all() + ->filter(fn ($sitemap) => $sitemap->urls()->isNotEmpty()) + ->toArray(); }); - return response($view)->withHeaders([ - 'Content-Type' => 'text/xml', - 'X-Robots-Tag' => 'noindex, nofollow', - ]); + 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'); } public function show(string $type, string $handle): Response { throw_unless(config('advanced-seo.sitemap.enabled'), new NotFoundHttpException); - throw_unless($sitemap = Sitemap::find("{$type}::{$handle}"), new NotFoundHttpException); + $id = "{$type}::{$handle}"; - $view = Cache::remember("advanced-seo::sitemaps::{$type}::{$handle}", Sitemap::cacheExpiry(), function () use ($sitemap) { - $urls = $sitemap->urls(); + $urls = Cache::remember( + "advanced-seo::sitemaps::{$id}", + Sitemap::cacheExpiry(), + fn () => Sitemap::find($id)?->urls()->toArray() + ); - throw_unless($urls->isNotEmpty(), new NotFoundHttpException); + throw_unless($urls, new NotFoundHttpException); - return view('advanced-seo::sitemaps.show', [ + return response() + ->view('advanced-seo::sitemaps.show', [ 'urls' => $urls, 'version' => Addon::get('aerni/advanced-seo')->version(), - ])->render(); - }); - - return response($view)->withHeaders([ - 'Content-Type' => 'text/xml', - 'X-Robots-Tag' => 'noindex, nofollow', - ]); + ]) + ->header('Content-Type', 'text/xml') + ->header('X-Robots-Tag', 'noindex, nofollow'); } public function xsl(): Response { - $path = __DIR__.'/../../../../resources/xsl/sitemap.xsl'; + throw_unless(config('advanced-seo.sitemap.enabled'), new NotFoundHttpException); + + $path = __DIR__.'/../../../../resources/views/sitemaps/sitemap.xsl'; - return response(file_get_contents($path))->withHeaders([ - 'Content-Type' => 'text/xsl', - 'X-Robots-Tag' => 'noindex, nofollow', - ]); + return response(file_get_contents($path)) + ->header('Content-Type', 'text/xsl') + ->header('X-Robots-Tag', 'noindex, nofollow'); } } diff --git a/src/Sitemap/CollectionSitemap.php b/src/Sitemap/CollectionSitemap.php deleted file mode 100644 index d9c1b39a..00000000 --- a/src/Sitemap/CollectionSitemap.php +++ /dev/null @@ -1,29 +0,0 @@ -entries() - ->map(fn ($entry) => (new CollectionSitemapUrl($entry, $this))->toArray()) - ->filter(); - } - - protected function entries(): Collection - { - return $this->model - ->queryEntries() - ->where('published', '!=', false) // We only want published entries. - ->where('uri', '!=', null) // We only want entries that have a route. This works for both single and per-site collection routes. - ->get() - ->filter(fn ($entry) => Indexable::handle($entry)); // We only want indexable entries. - } -} diff --git a/src/Sitemap/SitemapIndex.php b/src/Sitemap/SitemapIndex.php deleted file mode 100644 index 9eec6d07..00000000 --- a/src/Sitemap/SitemapIndex.php +++ /dev/null @@ -1,42 +0,0 @@ -push($sitemap) - ->unique(fn ($sitemap) => $sitemap->handle()) - ->toArray(); - } - - public function sitemaps(): Collection - { - return $this->collectionSitemaps() - ->merge($this->taxonomySitemaps()) - ->merge($this->customSitemaps()); - } - - public function collectionSitemaps(): Collection - { - return CollectionFacade::all()->map(fn ($collection) => new CollectionSitemap($collection)); - } - - public function taxonomySitemaps(): Collection - { - return TaxonomyFacade::all()->map(fn ($taxonomy) => new TaxonomySitemap($taxonomy)); - } - - public function customSitemaps(): Collection - { - return collect(self::$customSitemaps); - } -} diff --git a/src/Sitemap/SitemapRepository.php b/src/Sitemap/SitemapRepository.php deleted file mode 100644 index 7afb7ba1..00000000 --- a/src/Sitemap/SitemapRepository.php +++ /dev/null @@ -1,44 +0,0 @@ -sitemaps(); - } - - public function find(string $id): ?Sitemap - { - return $this->all()->first(fn ($sitemap) => $id === $sitemap->id()); - } - - public function clearCache(): void - { - $this->all()->each->clearCache(); - } - - public function cacheExpiry(): int - { - return config('advanced-seo.sitemap.expiry', 60) * 60; - } -} diff --git a/src/Sitemap/TaxonomySitemap.php b/src/Sitemap/TaxonomySitemap.php deleted file mode 100644 index 18f2b84c..00000000 --- a/src/Sitemap/TaxonomySitemap.php +++ /dev/null @@ -1,127 +0,0 @@ -taxonomyUrls() - ->merge($this->collectionTaxonomyUrls()) - ->merge($this->termUrls()) - ->merge($this->collectionTermUrls()) - ->values(); - } - - protected function taxonomyUrls(): Collection - { - return $this->taxonomies() - ->map(fn ($taxonomy, $site) => (new TaxonomySitemapUrl($taxonomy, $site, $this))->toArray()) - ->values(); - } - - protected function collectionTaxonomyUrls(): Collection - { - return $this->collectionTaxonomies() - ->map(fn ($item) => (new CollectionTaxonomySitemapUrl($item['taxonomy'], $item['site'], $this))->toArray()) - ->filter(); - } - - protected function termUrls(): Collection - { - return $this->terms($this->model) - ->map(fn ($term) => (new TermSitemapUrl($term, $this))->toArray()) - ->filter(); - } - - protected function collectionTermUrls(): Collection - { - return $this->collectionTerms() - ->map(fn ($term) => (new CollectionTermSitemapUrl($term, $this))->toArray()) - ->filter(); - } - - public function taxonomies(): Collection - { - // We only want to return the taxonomy if the template exists. - if (! view()->exists($this->model->template())) { - return collect(); - } - - return $this->model->sites() - ->mapWithKeys(fn ($site) => [$site => $this->model]) - ->filter(fn ($taxonomy, $site) => Indexable::handle($taxonomy, $site)); - } - - public function collectionTaxonomies(): Collection - { - $taxonomies = $this->taxonomyCollections(); - - // We only want to return the terms if the template exists. - if (! view()->exists($taxonomies->first()?->template())) { - return collect(); - } - - return $taxonomies->flatMap(function ($taxonomy) { - return $taxonomy->collection()->sites() - ->map(fn ($site) => [ - 'taxonomy' => $taxonomy, - 'site' => $site, - ]); - })->filter(fn ($item) => Indexable::handle($item['taxonomy'], $item['site'])); - } - - public function terms(Taxonomy $taxonomy): Collection - { - $terms = $taxonomy->queryTerms()->get(); - - // We only want to return the terms if the template exists. - if (! view()->exists($terms->first()?->template())) { - return collect(); - } - - return $terms - ->filter(fn ($term) => $term->taxonomy()->sites()->contains($term->locale())) // We only want terms of sites that are configured on the taxonomy. - ->filter(fn ($term) => Indexable::handle($term)); // We only want indexable terms. - } - - public function collectionTerms(): Collection - { - // Get the terms of each collection taxonomy. - $collectionTerms = $this->taxonomyCollections() - ->flatMap(fn ($taxonomy) => $this->terms($taxonomy)); - - // Filter the terms by the entries they are used on. - return $collectionTerms->filter(function ($term) { - return $term->queryEntries() - ->where('published', '!=', false) // We only want published entries. - ->where('uri', '!=', null) // We only want entries that have a route. This works for both single and per-site collection routes. - ->where('locale', '=', $term->locale()) // We only want entries with the same locale as the term. - ->get() - ->filter(fn ($entry) => Indexable::handle($entry)) - ->isNotEmpty(); - })->values(); - } - - protected function taxonomyCollections(): Collection - { - // Get all the collections that use this taxonomy. - $taxonomyCollections = $this->model->collections(); - - /** - * Attach each collection to a new instance of the taxonomy - * so that we can get the correct absolute URL of the collection terms later. - */ - return $taxonomyCollections->map(function ($collection) { - return $collection->taxonomies() - ->first(fn ($taxonomy) => $taxonomy->handle() === $this->handle()) - ->collection($collection); - }); - } -} diff --git a/src/Sitemap/BaseSitemap.php b/src/Sitemaps/BaseSitemap.php similarity index 63% rename from src/Sitemap/BaseSitemap.php rename to src/Sitemaps/BaseSitemap.php index e02880d1..095aab4a 100644 --- a/src/Sitemap/BaseSitemap.php +++ b/src/Sitemaps/BaseSitemap.php @@ -1,19 +1,23 @@ urls()->sortByDesc('lastmod')->first()['lastmod']; + return $this->urls()->sortByDesc('lastmod')->first()?->lastmod(); } public function clearCache(): void @@ -52,6 +56,24 @@ public function clearCache(): void Cache::forget("advanced-seo::sitemaps::{$this->id()}"); } + protected function includeInSitemapQuery(Builder $query): Builder + { + return $query + ->where('published', true) + ->whereNotNull('url') + ->where('seo_noindex', false) + ->where('seo_sitemap_enabled', true) + ->where('seo_canonical_type', 'current'); + } + + public function toArray(): array + { + return [ + 'url' => $this->url(), + 'lastmod' => $this->lastmod(), + ]; + } + public function __call(string $name, array $arguments): mixed { return $this->fluentlyGetOrSet($name)->args($arguments); diff --git a/src/Sitemap/BaseSitemapUrl.php b/src/Sitemaps/BaseSitemapUrl.php similarity index 65% rename from src/Sitemap/BaseSitemapUrl.php rename to src/Sitemaps/BaseSitemapUrl.php index ff505b57..625e3439 100644 --- a/src/Sitemap/BaseSitemapUrl.php +++ b/src/Sitemaps/BaseSitemapUrl.php @@ -1,15 +1,16 @@ isCanonicalUrl()) { - return null; - } - return [ 'loc' => $this->loc(), 'alternates' => $this->alternates(), diff --git a/src/Sitemaps/Collections/CollectionSitemap.php b/src/Sitemaps/Collections/CollectionSitemap.php new file mode 100644 index 00000000..269cc70a --- /dev/null +++ b/src/Sitemaps/Collections/CollectionSitemap.php @@ -0,0 +1,32 @@ +urls)) { + return $this->urls; + } + + return $this->urls = $this->entries() + ->map(fn ($entry) => new EntrySitemapUrl($entry, $this)) + ->filter(fn ($url) => $url->canonicalTypeIsCurrent()); + } + + protected function entries(): Collection + { + return $this->model->queryEntries() + ->where($this->includeInSitemapQuery(...)) + ->get() + ->filter(IncludeInSitemap::run(...)); + } +} diff --git a/src/Sitemap/CollectionSitemapUrl.php b/src/Sitemaps/Collections/EntrySitemapUrl.php similarity index 74% rename from src/Sitemap/CollectionSitemapUrl.php rename to src/Sitemaps/Collections/EntrySitemapUrl.php index 84b98e81..717e1d18 100644 --- a/src/Sitemap/CollectionSitemapUrl.php +++ b/src/Sitemaps/Collections/EntrySitemapUrl.php @@ -1,13 +1,14 @@ absoluteUrl($this->entry); } + // TODO: Can we use the entryAndTermHreflang method from the HasHreflang trait as the code is just a copy of it. public function alternates(): ?array { if (! Site::multiEnabled()) { return null; } - if (! Indexable::handle($this->entry)) { - return null; - } - $sites = $this->entry->sites(); if ($sites->count() < 2) { @@ -35,7 +33,7 @@ public function alternates(): ?array $hreflang = $sites ->map(fn ($locale) => $this->entry->in($locale)) ->filter() // A model might not exist in a site. So we need to remove it to prevent calling methods on null - ->filter(Indexable::handle(...)); + ->filter(IncludeInSitemap::run(...)); if ($hreflang->count() < 2) { return null; @@ -48,7 +46,7 @@ public function alternates(): ?array $origin = $this->entry->origin() ?? $this->entry; - $xDefault = Indexable::handle($origin) ? $origin : $this->entry; + $xDefault = IncludeInSitemap::run($origin) ? $origin : $this->entry; return $hreflang->push([ 'href' => $this->absoluteUrl($xDefault), @@ -74,14 +72,11 @@ public function priority(): string public function site(): string { - return $this->entry->site()->handle(); + return $this->entry->locale(); } - public function isCanonicalUrl(): bool + public function canonicalTypeIsCurrent(): bool { - return match ($this->entry->seo_canonical_type->value()) { - 'current' => true, - default => false, - }; + return $this->entry->seo_canonical_type == 'current'; } } diff --git a/src/Sitemap/CustomSitemap.php b/src/Sitemaps/Custom/CustomSitemap.php similarity index 76% rename from src/Sitemap/CustomSitemap.php rename to src/Sitemaps/Custom/CustomSitemap.php index db389e8d..8eeddcfe 100644 --- a/src/Sitemap/CustomSitemap.php +++ b/src/Sitemaps/Custom/CustomSitemap.php @@ -1,13 +1,12 @@ urls = collect(); @@ -22,6 +21,6 @@ public function add(CustomSitemapUrl $item): self public function urls(): Collection { - return $this->urls->map->toArray(); + return $this->urls; } } diff --git a/src/Sitemap/CustomSitemapUrl.php b/src/Sitemaps/Custom/CustomSitemapUrl.php similarity index 97% rename from src/Sitemap/CustomSitemapUrl.php rename to src/Sitemaps/Custom/CustomSitemapUrl.php index 7e8c7636..3a9fde07 100644 --- a/src/Sitemap/CustomSitemapUrl.php +++ b/src/Sitemaps/Custom/CustomSitemapUrl.php @@ -1,8 +1,9 @@ customSitemaps = $this->customSitemaps() + ->push($sitemap) + ->unique(fn ($sitemap) => $sitemap->handle()) + ->all(); + } + + public function all(): Collection + { + return $this->collectionSitemaps() + ->merge($this->taxonomySitemaps()) + ->merge($this->customSitemaps()); + } + + public function find(string $id): ?Sitemap + { + $method = Str::before($id, '::').'Sitemaps'; + + if (! method_exists($this, $method)) { + return null; + } + + return $this->$method()->first(fn ($sitemap) => $id === $sitemap->id()); + } + + public function collectionSitemaps(): Collection + { + return CollectionFacade::all() + ->filter(IsEnabledModel::handle(...)) + ->mapInto(CollectionSitemap::class) + ->values(); + } + + public function taxonomySitemaps(): Collection + { + return Taxonomy::all() + ->filter(IsEnabledModel::handle(...)) + ->mapInto(TaxonomySitemap::class) + ->values(); + } + + public function customSitemaps(): Collection + { + return collect($this->customSitemaps); + } + + public function clearCache(): void + { + $this->all()->each->clearCache(); + } + + public function cacheExpiry(): int + { + return config('advanced-seo.sitemap.expiry', 60) * 60; + } +} diff --git a/src/Sitemap/CollectionTaxonomySitemapUrl.php b/src/Sitemaps/Taxonomies/CollectionTaxonomySitemapUrl.php similarity index 72% rename from src/Sitemap/CollectionTaxonomySitemapUrl.php rename to src/Sitemaps/Taxonomies/CollectionTaxonomySitemapUrl.php index dd2fc16f..34e27744 100644 --- a/src/Sitemap/CollectionTaxonomySitemapUrl.php +++ b/src/Sitemaps/Taxonomies/CollectionTaxonomySitemapUrl.php @@ -1,10 +1,12 @@ getUrl($this->taxonomy, $this->site); + return $this->collectionTaxonomyUrl($this->taxonomy, $this->site); } public function alternates(): ?array @@ -32,7 +34,7 @@ public function alternates(): ?array } $hreflang = $sites->map(fn ($site) => [ - 'href' => $this->getUrl($this->taxonomy, $site), + 'href' => $this->collectionTaxonomyUrl($this->taxonomy, $site), 'hreflang' => Helpers::parseLocale(Site::get($site)->locale()), ]); @@ -41,18 +43,21 @@ public function alternates(): ?array $xDefaultSite = $sites->contains($originSite) ? $originSite : $this->site; return $hreflang->push([ - 'href' => $this->getUrl($this->taxonomy, $xDefaultSite), + 'href' => $this->collectionTaxonomyUrl($this->taxonomy, $xDefaultSite), 'hreflang' => 'x-default', ])->values()->all(); } public function lastmod(): string { - if ($terms = $this->lastModifiedTaxonomyTerm()) { - return $terms->lastModified()->format('Y-m-d\TH:i:sP'); + if ($term = $this->lastModifiedTaxonomyTerm()) { + return $term->lastModified()->format('Y-m-d\TH:i:sP'); } - return now()->format('Y-m-d\TH:i:sP'); + return Cache::rememberForever( + "advanced-seo::sitemaps::taxonomy::{$this->taxonomy}::lastmod", + fn () => now()->format('Y-m-d\TH:i:sP') + ); } public function changefreq(): string @@ -74,8 +79,7 @@ protected function lastModifiedTaxonomyTerm(): ?Term { return $this->taxonomy->queryTerms() ->where('site', $this->site) - ->get() - ->sortByDesc(fn ($term) => $term->lastModified()) + ->orderByDesc('last_modified') ->first(); } @@ -89,7 +93,8 @@ protected function taxonomies(): Collection ->mapwithKeys(fn ($item) => [$item['site'] => $item['taxonomy']]); } - protected function getUrl(Taxonomy $taxonomy, string $site): string + // TODO: Should be able to remove this once https://github.com/statamic/cms/pull/10439 is merged. + protected function collectionTaxonomyUrl(Taxonomy $taxonomy, string $site): string { $siteUrl = $this->absoluteUrl(Site::get($site)); $taxonomyHandle = $taxonomy->handle(); diff --git a/src/Sitemap/CollectionTermSitemapUrl.php b/src/Sitemaps/Taxonomies/CollectionTermSitemapUrl.php similarity index 73% rename from src/Sitemap/CollectionTermSitemapUrl.php rename to src/Sitemaps/Taxonomies/CollectionTermSitemapUrl.php index af641a38..dbb7ff62 100644 --- a/src/Sitemap/CollectionTermSitemapUrl.php +++ b/src/Sitemaps/Taxonomies/CollectionTermSitemapUrl.php @@ -1,8 +1,10 @@ term->origin(); - $xDefault = Indexable::handle($origin) ? $origin : $this->term; + $xDefault = IncludeInSitemap::run($origin) ? $origin : $this->term; return $hreflang->push([ 'href' => $this->absoluteUrl($xDefault), @@ -51,26 +53,17 @@ public function lastmod(): string public function changefreq(): string { - return $this->term->seo_sitemap_change_frequency; + return Defaults::data('taxonomies')->get('seo_sitemap_change_frequency'); } public function priority(): string { - // Make sure we actually return `0.0` and `1.0`. - return number_format($this->term->seo_sitemap_priority->value(), 1); + return Defaults::data('taxonomies')->get('seo_sitemap_priority'); } public function site(): string { - return $this->term->site()->handle(); - } - - public function isCanonicalUrl(): bool - { - return match ($this->term->seo_canonical_type->value()) { - 'current' => true, - default => false, - }; + return $this->term->locale(); } protected function terms(): Collection diff --git a/src/Sitemaps/Taxonomies/TaxonomySitemap.php b/src/Sitemaps/Taxonomies/TaxonomySitemap.php new file mode 100644 index 00000000..449bb290 --- /dev/null +++ b/src/Sitemaps/Taxonomies/TaxonomySitemap.php @@ -0,0 +1,115 @@ +urls)) { + return $this->urls; + } + + return $this->urls = $this->taxonomyUrls() + ->merge($this->termUrls()) + ->merge($this->collectionTaxonomyUrls()) + ->merge($this->collectionTermUrls()) + ->filter(fn ($url) => $url->canonicalTypeIsCurrent()); + } + + protected function taxonomyUrls(): Collection + { + return $this->taxonomies() + ->map(fn ($taxonomy, $site) => new TaxonomySitemapUrl($taxonomy, $site, $this)) + ->values(); + } + + protected function termUrls(): Collection + { + return $this->terms() + ->map(fn ($term) => new TermSitemapUrl($term, $this)); + } + + protected function collectionTaxonomyUrls(): Collection + { + return $this->collectionTaxonomies() + ->map(fn ($item) => new CollectionTaxonomySitemapUrl($item['taxonomy'], $item['site'], $this)); + } + + protected function collectionTermUrls(): Collection + { + return $this->collectionTerms() + ->map(fn ($term) => new CollectionTermSitemapUrl($term, $this)); + } + + public function taxonomies(): Collection + { + if (! view()->exists($this->model->template())) { + return collect(); + } + + return $this->model->sites() + ->filter(fn ($site) => IncludeInSitemap::run($this->model, $site)) + ->mapWithKeys(fn ($site) => [$site => $this->model]); + } + + protected function terms(): Collection + { + $terms = $this->model + ->queryTerms() + ->where($this->includeInSitemapQuery(...)) + ->get(); + + if (! view()->exists($terms->first()?->template())) { + return collect(); + } + + return $terms->filter(IncludeInSitemap::run(...)); + } + + public function collectionTaxonomies(): Collection + { + return $this->model->collections() + ->map(fn ($collection) => $this->freshTaxonomy()->collection($collection)) // Need to get a fresh instance of the Taxonomy, else we'll override the previously set collection. + ->filter(fn ($taxonomy) => view()->exists($taxonomy->template())) + ->flatMap(function ($taxonomy) { + // Only allow sites that have been set on both the taxonomy and the collection + return $taxonomy->sites() + ->merge($taxonomy->collection()->sites()) + ->duplicates() + ->map(fn ($site) => ['taxonomy' => $taxonomy, 'site' => $site]); + }) + ->filter(fn ($item) => IncludeInSitemap::run($item['taxonomy'], $item['site'])); + } + + public function collectionTerms(): Collection + { + $terms = $this->model->queryTerms()->get(); + + return $this->model->collections() + ->flatMap(function ($collection) use ($terms) { + return $terms->map(fn ($term) => $term->fresh()->collection($collection)); // Need to get a fresh instance of the Term, else we'll override the previously set collection. + }) + ->filter(fn ($term) => view()->exists($term->template())) + ->filter(function ($term) { + // Only allow sites that have been set on both the taxonomy and the collection + return $term->taxonomy()->sites() + ->merge($term->collection()->sites()) + ->duplicates() + ->contains($term->locale()); + }) + ->filter(fn ($term) => IncludeInSitemap::run($term->taxonomy(), $term->locale())); + } + + protected function freshTaxonomy(): Taxonomy + { + return \Statamic\Facades\Taxonomy::find($this->model->id()); + } +} diff --git a/src/Sitemap/TaxonomySitemapUrl.php b/src/Sitemaps/Taxonomies/TaxonomySitemapUrl.php similarity index 70% rename from src/Sitemap/TaxonomySitemapUrl.php rename to src/Sitemaps/Taxonomies/TaxonomySitemapUrl.php index 1d8abfe4..c0368bd1 100644 --- a/src/Sitemap/TaxonomySitemapUrl.php +++ b/src/Sitemaps/Taxonomies/TaxonomySitemapUrl.php @@ -1,33 +1,23 @@ initialSite = Site::current()->handle(); - - // We need to set the site so that we can get to correct URL of the taxonomy. - Site::setCurrent($site); - } - - public function __destruct() - { - Site::setCurrent($this->initialSite); - } + public function __construct(protected Taxonomy $taxonomy, protected string $site, protected TaxonomySitemap $sitemap) {} public function loc(): string { + Site::setCurrent($this->site); + return $this->absoluteUrl($this->taxonomy); } @@ -37,7 +27,7 @@ public function alternates(): ?array return null; } - $sites = $this->taxonomies()->keys(); + $sites = $this->sitemap->taxonomies()->keys(); if ($sites->count() < 2) { return null; @@ -68,11 +58,14 @@ public function alternates(): ?array public function lastmod(): string { - if ($terms = $this->lastModifiedTaxonomyTerm()) { - return $terms->lastModified()->format('Y-m-d\TH:i:sP'); + if ($term = $this->lastModifiedTaxonomyTerm()) { + return $term->lastModified()->format('Y-m-d\TH:i:sP'); } - return now()->format('Y-m-d\TH:i:sP'); + return Cache::rememberForever( + "advanced-seo::sitemaps::taxonomy::{$this->taxonomy}::lastmod", + fn () => now()->format('Y-m-d\TH:i:sP') + ); } public function changefreq(): string @@ -94,13 +87,7 @@ protected function lastModifiedTaxonomyTerm(): ?Term { return $this->taxonomy->queryTerms() ->where('site', $this->site) - ->get() - ->sortByDesc(fn ($term) => $term->lastModified()) + ->orderByDesc('last_modified') ->first(); } - - protected function taxonomies(): Collection - { - return $this->sitemap->taxonomies(); - } } diff --git a/src/Sitemap/TermSitemapUrl.php b/src/Sitemaps/Taxonomies/TermSitemapUrl.php similarity index 68% rename from src/Sitemap/TermSitemapUrl.php rename to src/Sitemaps/Taxonomies/TermSitemapUrl.php index b6813497..40c1fde9 100644 --- a/src/Sitemap/TermSitemapUrl.php +++ b/src/Sitemaps/Taxonomies/TermSitemapUrl.php @@ -1,10 +1,10 @@ terms(); + $terms = $this->term->term() + ->localizations() + ->filter(IncludeInSitemap::run(...)); if ($terms->count() < 2) { return null; @@ -36,7 +38,7 @@ public function alternates(): ?array $origin = $this->term->origin(); - $xDefault = Indexable::handle($origin) ? $origin : $this->term; + $xDefault = IncludeInSitemap::run($origin) ? $origin : $this->term; return $hreflang->push([ 'href' => $this->absoluteUrl($xDefault), @@ -62,21 +64,11 @@ public function priority(): string public function site(): string { - return $this->term->site()->handle(); + return $this->term->locale(); } - public function isCanonicalUrl(): bool + public function canonicalTypeIsCurrent(): bool { - return match ($this->term->seo_canonical_type->value()) { - 'current' => true, - default => false, - }; - } - - protected function terms(): Collection - { - return $this->sitemap - ->terms($this->term->taxonomy()) - ->filter(fn ($term) => $term->id() === $this->term->id()); + return $this->term->seo_canonical_type == 'current'; } } diff --git a/src/View/Concerns/EvaluatesContextType.php b/src/View/Concerns/EvaluatesContextType.php deleted file mode 100644 index 46766ff3..00000000 --- a/src/View/Concerns/EvaluatesContextType.php +++ /dev/null @@ -1,26 +0,0 @@ -model->value('is_entry') || $this->model->value('is_term'); - } - - protected function contextIsTaxonomy(): bool - { - return $this->model->get('page') instanceof Taxonomy - && $this->model->get('page')->collection() === null; - } - - protected function contextIsCollectionTaxonomy(): bool - { - return $this->model->get('page') instanceof Taxonomy - && $this->model->get('page')->collection() instanceof Collection; - } -} diff --git a/src/View/Concerns/HasHreflang.php b/src/View/Concerns/HasHreflang.php index 6203667f..bc289074 100644 --- a/src/View/Concerns/HasHreflang.php +++ b/src/View/Concerns/HasHreflang.php @@ -2,6 +2,7 @@ namespace Aerni\AdvancedSeo\View\Concerns; +use Aerni\AdvancedSeo\Concerns\EvaluatesIndexability; use Aerni\AdvancedSeo\Support\Helpers; use Statamic\Contracts\Entries\Entry; use Statamic\Facades\Site; @@ -128,6 +129,7 @@ protected function collectionTaxonomyHreflang(Taxonomy $taxonomy): ?array ])->values()->all(); } + // TODO: Should be able to remove this once https://github.com/statamic/cms/pull/10439 is merged. protected function collectionTaxonomyUrl(Taxonomy $taxonomy, string $site): string { $siteUrl = Site::get($site)->absoluteUrl(); @@ -139,11 +141,8 @@ protected function collectionTaxonomyUrl(Taxonomy $taxonomy, string $site): stri protected function shouldIncludeHreflang(Entry|LocalizedTerm $model): bool { - if ($this->canonicalPointsToAnotherUrl($model)) { - return false; - } - - return $this->isIndexable($model); + return ! $this->canonicalPointsToAnotherUrl($model) + && $this->isIndexableEntryOrTerm($model); } protected function canonicalPointsToAnotherUrl(Entry|LocalizedTerm $model): bool diff --git a/src/View/GraphQlCascade.php b/src/View/GraphQlCascade.php index 0f9b5368..10183cfe 100644 --- a/src/View/GraphQlCascade.php +++ b/src/View/GraphQlCascade.php @@ -2,11 +2,11 @@ namespace Aerni\AdvancedSeo\View; +use Aerni\AdvancedSeo\Concerns\EvaluatesIndexability; use Aerni\AdvancedSeo\Concerns\HasBaseUrl; use Aerni\AdvancedSeo\Data\HasComputedData; use Aerni\AdvancedSeo\Facades\SocialImage; use Aerni\AdvancedSeo\Support\Helpers; -use Aerni\AdvancedSeo\View\Concerns\EvaluatesIndexability; use Aerni\AdvancedSeo\View\Concerns\HasAbsoluteUrl; use Aerni\AdvancedSeo\View\Concerns\HasHreflang; use Illuminate\Support\Collection; diff --git a/src/View/ViewCascade.php b/src/View/ViewCascade.php index 7099994f..89aff2cc 100644 --- a/src/View/ViewCascade.php +++ b/src/View/ViewCascade.php @@ -2,12 +2,12 @@ namespace Aerni\AdvancedSeo\View; +use Aerni\AdvancedSeo\Concerns\EvaluatesContextType; +use Aerni\AdvancedSeo\Concerns\EvaluatesIndexability; use Aerni\AdvancedSeo\Data\HasComputedData; use Aerni\AdvancedSeo\Facades\SocialImage; use Aerni\AdvancedSeo\Models\Defaults; use Aerni\AdvancedSeo\Support\Helpers; -use Aerni\AdvancedSeo\View\Concerns\EvaluatesContextType; -use Aerni\AdvancedSeo\View\Concerns\EvaluatesIndexability; use Aerni\AdvancedSeo\View\Concerns\HasHreflang; use Illuminate\Support\Collection; use Spatie\SchemaOrg\Schema; @@ -191,9 +191,9 @@ public function hreflang(): ?array } return match (true) { - ($this->contextIsEntryOrTerm()) => $this->entryAndTermHreflang($this->model->get('id')->resolve()->augmentable()), // TODO: Remove resolve() once https://github.com/statamic/cms/pull/10417 is merged. - ($this->contextIsTaxonomy()) => $this->taxonomyHreflang($this->model->get('page')), - ($this->contextIsCollectionTaxonomy()) => $this->collectionTaxonomyHreflang($this->model->get('page')), + ($this->contextIsEntryOrTerm($this->model)) => $this->entryAndTermHreflang($this->model->get('id')->augmentable()), + ($this->contextIsTaxonomy($this->model)) => $this->taxonomyHreflang($this->model->get('page')), + ($this->contextIsCollectionTaxonomy($this->model)) => $this->collectionTaxonomyHreflang($this->model->get('page')), default => null }; }