diff --git a/lang/en/fields.php b/lang/en/fields.php index edcc1b28..c012a395 100644 --- a/lang/en/fields.php +++ b/lang/en/fields.php @@ -140,8 +140,8 @@ 'seo_canonical_custom' => [ 'display' => 'URL', - 'instructions' => 'A fully qualified URL starting with https://.', - 'default_instructions' => 'A fully qualified URL starting with https://.', + 'instructions' => 'A fully qualified and [active URL](https://laravel.com/docs/11.x/validation#rule-active-url).', + 'default_instructions' => 'A fully qualified and [active URL](https://laravel.com/docs/11.x/validation#rule-active-url).', ], 'seo_section_indexing' => [ diff --git a/resources/fieldsets/main.yaml b/resources/fieldsets/main.yaml index 6d1df602..0fbcf7ae 100644 --- a/resources/fieldsets/main.yaml +++ b/resources/fieldsets/main.yaml @@ -8,10 +8,10 @@ fields: import: 'advanced-seo::open_graph' - import: 'advanced-seo::twitter' - - - import: 'advanced-seo::canonical_url' - import: 'advanced-seo::indexing' + - + import: 'advanced-seo::canonical_url' - import: 'advanced-seo::sitemap' - diff --git a/src/Actions/Indexable.php b/src/Actions/Indexable.php index 6d51bedf..a8017ad7 100644 --- a/src/Actions/Indexable.php +++ b/src/Actions/Indexable.php @@ -3,6 +3,7 @@ namespace Aerni\AdvancedSeo\Actions; use Aerni\AdvancedSeo\Facades\Seo; +use Aerni\AdvancedSeo\View\Concerns\EvaluatesIndexability; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Taxonomies\Taxonomy; use Statamic\Contracts\Taxonomies\Term; @@ -10,8 +11,14 @@ class Indexable { + use EvaluatesIndexability; + public static function handle(Entry|Term|Taxonomy $model, ?string $locale = null): bool { + if (! (new self)->crawlingIsEnabled()) { + return false; + } + $locale = $locale ?? $model->locale(); return Blink::once("{$model->id()}::{$locale}", function () use ($model, $locale) { diff --git a/src/Fields/ContentDefaultsFields.php b/src/Fields/ContentDefaultsFields.php index 7575d4a4..30601741 100644 --- a/src/Fields/ContentDefaultsFields.php +++ b/src/Fields/ContentDefaultsFields.php @@ -21,8 +21,8 @@ protected function sections(): array $this->socialImagesGenerator(), $this->openGraphImage(), $this->twitterImage(), - $this->canonicalUrl(), $this->indexing(), + $this->canonicalUrl(), $this->sitemap(), $this->jsonLd(), ]; @@ -283,6 +283,44 @@ protected function twitterImage(): array ]; } + protected function indexing(): array + { + return [ + [ + 'handle' => 'seo_section_indexing', + 'field' => [ + 'type' => 'section', + 'display' => $this->trans('seo_section_indexing.display'), + 'instructions' => $this->trans('seo_section_indexing.default_instructions'), + ], + ], + [ + 'handle' => 'seo_noindex', + 'field' => [ + 'type' => 'toggle', + 'display' => $this->trans('seo_noindex.display'), + 'instructions' => $this->trans('seo_noindex.default_instructions'), + 'default' => Defaults::data('collections')->get('seo_noindex'), + 'listable' => 'hidden', + 'localizable' => true, + 'width' => 50, + ], + ], + [ + 'handle' => 'seo_nofollow', + 'field' => [ + 'type' => 'toggle', + 'display' => $this->trans('seo_nofollow.display'), + 'instructions' => $this->trans('seo_nofollow.default_instructions'), + 'default' => Defaults::data('collections')->get('seo_nofollow'), + 'listable' => 'hidden', + 'localizable' => true, + 'width' => 50, + ], + ], + ]; + } + protected function canonicalUrl(): array { return [ @@ -347,44 +385,6 @@ protected function canonicalUrl(): array ]; } - protected function indexing(): array - { - return [ - [ - 'handle' => 'seo_section_indexing', - 'field' => [ - 'type' => 'section', - 'display' => $this->trans('seo_section_indexing.display'), - 'instructions' => $this->trans('seo_section_indexing.default_instructions'), - ], - ], - [ - 'handle' => 'seo_noindex', - 'field' => [ - 'type' => 'toggle', - 'display' => $this->trans('seo_noindex.display'), - 'instructions' => $this->trans('seo_noindex.default_instructions'), - 'default' => Defaults::data('collections')->get('seo_noindex'), - 'listable' => 'hidden', - 'localizable' => true, - 'width' => 50, - ], - ], - [ - 'handle' => 'seo_nofollow', - 'field' => [ - 'type' => 'toggle', - 'display' => $this->trans('seo_nofollow.display'), - 'instructions' => $this->trans('seo_nofollow.default_instructions'), - 'default' => Defaults::data('collections')->get('seo_nofollow'), - 'listable' => 'hidden', - 'localizable' => true, - 'width' => 50, - ], - ], - ]; - } - protected function sitemap(): array { return [ diff --git a/src/Fields/OnPageSeoFields.php b/src/Fields/OnPageSeoFields.php index e75d0f70..357e04b2 100644 --- a/src/Fields/OnPageSeoFields.php +++ b/src/Fields/OnPageSeoFields.php @@ -20,8 +20,8 @@ protected function sections(): array $this->socialImagesGenerator(), $this->openGraphImage(), $this->twitterImage(), - $this->canonicalUrl(), $this->indexing(), + $this->canonicalUrl(), $this->sitemap(), $this->jsonLd(), ]; @@ -379,6 +379,50 @@ protected function twitterImage(): array ]; } + protected function indexing(): array + { + return [ + [ + 'handle' => 'seo_section_indexing', + 'field' => [ + 'type' => 'section', + 'display' => $this->trans('seo_section_indexing.display'), + 'instructions' => $this->trans('seo_section_indexing.instructions'), + ], + ], + [ + 'handle' => 'seo_noindex', + 'field' => [ + 'type' => 'seo_source', + 'display' => $this->trans('seo_noindex.display'), + 'instructions' => $this->trans('seo_noindex.instructions'), + 'default' => '@default', + 'localizable' => true, + 'classes' => 'toggle-fieldtype', + 'width' => 50, + 'field' => [ + 'type' => 'toggle', + ], + ], + ], + [ + 'handle' => 'seo_nofollow', + 'field' => [ + 'type' => 'seo_source', + 'display' => $this->trans('seo_nofollow.display'), + 'instructions' => $this->trans('seo_nofollow.instructions'), + 'default' => '@default', + 'localizable' => true, + 'classes' => 'toggle-fieldtype', + 'width' => 50, + 'field' => [ + 'type' => 'toggle', + ], + ], + ], + ]; + } + protected function canonicalUrl(): array { return [ @@ -388,6 +432,9 @@ protected function canonicalUrl(): array 'type' => 'section', 'display' => $this->trans('seo_section_canonical_url.display'), 'instructions' => $this->trans('seo_section_canonical_url.instructions'), + 'if' => [ + 'seo_noindex.value' => 'false', + ], ], ], [ @@ -399,6 +446,9 @@ protected function canonicalUrl(): array 'default' => '@default', 'localizable' => true, 'classes' => 'button_group-fieldtype', + 'if' => [ + 'seo_noindex.value' => 'false', + ], 'field' => [ 'type' => 'button_group', 'options' => [ @@ -419,6 +469,7 @@ protected function canonicalUrl(): array 'localizable' => true, 'classes' => 'relationship-fieldtype', 'if' => [ + 'seo_noindex.value' => 'false', 'seo_canonical_type.value' => 'equals other', ], 'field' => [ @@ -443,6 +494,7 @@ protected function canonicalUrl(): array 'classes' => 'text-fieldtype', 'antlers' => true, 'if' => [ + 'seo_noindex.value' => 'false', 'seo_canonical_type.value' => 'equals custom', ], 'field' => [ @@ -450,6 +502,7 @@ protected function canonicalUrl(): array 'input_type' => 'url', 'validate' => [ 'required_if:seo_canonical_type,custom', + 'active_url', ], ], ], @@ -457,50 +510,6 @@ protected function canonicalUrl(): array ]; } - protected function indexing(): array - { - return [ - [ - 'handle' => 'seo_section_indexing', - 'field' => [ - 'type' => 'section', - 'display' => $this->trans('seo_section_indexing.display'), - 'instructions' => $this->trans('seo_section_indexing.instructions'), - ], - ], - [ - 'handle' => 'seo_noindex', - 'field' => [ - 'type' => 'seo_source', - 'display' => $this->trans('seo_noindex.display'), - 'instructions' => $this->trans('seo_noindex.instructions'), - 'default' => '@default', - 'localizable' => true, - 'classes' => 'toggle-fieldtype', - 'width' => 50, - 'field' => [ - 'type' => 'toggle', - ], - ], - ], - [ - 'handle' => 'seo_nofollow', - 'field' => [ - 'type' => 'seo_source', - 'display' => $this->trans('seo_nofollow.display'), - 'instructions' => $this->trans('seo_nofollow.instructions'), - 'default' => '@default', - 'localizable' => true, - 'classes' => 'toggle-fieldtype', - 'width' => 50, - 'field' => [ - 'type' => 'toggle', - ], - ], - ], - ]; - } - protected function sitemap(): array { return [ diff --git a/src/Sitemap/CollectionSitemapUrl.php b/src/Sitemap/CollectionSitemapUrl.php index 716a89e0..84b98e81 100644 --- a/src/Sitemap/CollectionSitemapUrl.php +++ b/src/Sitemap/CollectionSitemapUrl.php @@ -4,7 +4,6 @@ use Aerni\AdvancedSeo\Actions\Indexable; use Aerni\AdvancedSeo\Support\Helpers; -use Illuminate\Support\Collection; use Statamic\Contracts\Entries\Entry; use Statamic\Facades\Site; @@ -19,22 +18,42 @@ public function loc(): string public function alternates(): ?array { - $entries = $this->entries(); + if (! Site::multiEnabled()) { + return null; + } - // We only want alternate URLs if there are at least two entries. - if ($entries->count() <= 1) { + if (! Indexable::handle($this->entry)) { return null; } - return $entries->map(fn ($entry) => [ - 'hreflang' => Helpers::parseLocale(Site::get($entry->locale())->locale()), - 'href' => $this->absoluteUrl($entry), - ]) - ->put('x-default', [ - 'hreflang' => 'x-default', - 'href' => $this->absoluteUrl($this->entry->origin() ?? $this->entry), - ]) - ->toArray(); + $sites = $this->entry->sites(); + + if ($sites->count() < 2) { + return null; + } + + $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(...)); + + if ($hreflang->count() < 2) { + return null; + } + + $hreflang->transform(fn ($model) => [ + 'href' => $this->absoluteUrl($model), + 'hreflang' => Helpers::parseLocale($model->site()->locale()), + ]); + + $origin = $this->entry->origin() ?? $this->entry; + + $xDefault = Indexable::handle($origin) ? $origin : $this->entry; + + return $hreflang->push([ + 'href' => $this->absoluteUrl($xDefault), + 'hreflang' => 'x-default', + ])->values()->all(); } public function lastmod(): string @@ -65,13 +84,4 @@ public function isCanonicalUrl(): bool default => false, }; } - - protected function entries(): Collection - { - $root = $this->entry->root(); - $descendants = $root->descendants(); - - return collect([$root->locale() => $root])->merge($descendants) - ->filter(fn ($entry) => Indexable::handle($entry)); - } } diff --git a/src/Sitemap/CollectionTaxonomySitemapUrl.php b/src/Sitemap/CollectionTaxonomySitemapUrl.php index 3541e7c6..dd2fc16f 100644 --- a/src/Sitemap/CollectionTaxonomySitemapUrl.php +++ b/src/Sitemap/CollectionTaxonomySitemapUrl.php @@ -21,24 +21,29 @@ public function loc(): string public function alternates(): ?array { - $taxonomies = $this->taxonomies(); + if (! Site::multiEnabled()) { + return null; + } + + $sites = $this->taxonomies()->keys(); - // We only want alternate URLs if there are at least two terms. - if ($taxonomies->count() <= 1) { + if ($sites->count() < 2) { return null; } - return $taxonomies->map(function ($taxonomy, $site) { - return [ - 'hreflang' => Helpers::parseLocale(Site::get($site)->locale()), - 'href' => $this->getUrl($taxonomy, $site), - ]; - }) - ->put('x-default', [ - 'hreflang' => 'x-default', - 'href' => $this->getUrl($this->taxonomy, $this->taxonomy->sites()->first()), - ]) - ->toArray(); + $hreflang = $sites->map(fn ($site) => [ + 'href' => $this->getUrl($this->taxonomy, $site), + 'hreflang' => Helpers::parseLocale(Site::get($site)->locale()), + ]); + + $originSite = $this->taxonomy->sites()->first(); + + $xDefaultSite = $sites->contains($originSite) ? $originSite : $this->site; + + return $hreflang->push([ + 'href' => $this->getUrl($this->taxonomy, $xDefaultSite), + 'hreflang' => 'x-default', + ])->values()->all(); } public function lastmod(): string diff --git a/src/Sitemap/CollectionTermSitemapUrl.php b/src/Sitemap/CollectionTermSitemapUrl.php index 5da1d47f..af641a38 100644 --- a/src/Sitemap/CollectionTermSitemapUrl.php +++ b/src/Sitemap/CollectionTermSitemapUrl.php @@ -2,6 +2,7 @@ namespace Aerni\AdvancedSeo\Sitemap; +use Aerni\AdvancedSeo\Actions\Indexable; use Aerni\AdvancedSeo\Support\Helpers; use Illuminate\Support\Collection; use Statamic\Contracts\Taxonomies\Term; @@ -18,22 +19,29 @@ public function loc(): string public function alternates(): ?array { + if (! Site::multiEnabled()) { + return null; + } + $terms = $this->terms(); - // We only want alternate URLs if there are at least two terms. - if ($terms->count() <= 1) { + if ($terms->count() < 2) { return null; } - return $terms->map(fn ($term) => [ - 'hreflang' => Helpers::parseLocale(Site::get($term->locale())->locale()), + $hreflang = $terms->map(fn ($term) => [ 'href' => $this->absoluteUrl($term), - ]) - ->put('x-default', [ - 'hreflang' => 'x-default', - 'href' => $this->absoluteUrl($this->term->origin()), - ]) - ->toArray(); + 'hreflang' => Helpers::parseLocale($term->site()->locale()), + ]); + + $origin = $this->term->origin(); + + $xDefault = Indexable::handle($origin) ? $origin : $this->term; + + return $hreflang->push([ + 'href' => $this->absoluteUrl($xDefault), + 'hreflang' => 'x-default', + ])->values()->all(); } public function lastmod(): string diff --git a/src/Sitemap/TaxonomySitemapUrl.php b/src/Sitemap/TaxonomySitemapUrl.php index b674fb8c..1d8abfe4 100644 --- a/src/Sitemap/TaxonomySitemapUrl.php +++ b/src/Sitemap/TaxonomySitemapUrl.php @@ -11,16 +11,19 @@ class TaxonomySitemapUrl extends BaseSitemapUrl { + protected string $initialSite; + public function __construct(protected Taxonomy $taxonomy, protected string $site, protected TaxonomySitemap $sitemap) { + $this->initialSite = Site::current()->handle(); + // We need to set the site so that we can get to correct URL of the taxonomy. - $this->previousSite = Site::current()->handle(); Site::setCurrent($site); } public function __destruct() { - Site::setCurrent($this->previousSite); + Site::setCurrent($this->initialSite); } public function loc(): string @@ -30,30 +33,37 @@ public function loc(): string public function alternates(): ?array { - $taxonomies = $this->taxonomies(); + if (! Site::multiEnabled()) { + return null; + } + + $sites = $this->taxonomies()->keys(); - // We only want alternate URLs if there are at least two terms. - if ($taxonomies->count() <= 1) { + if ($sites->count() < 2) { return null; } - $hreflang = $taxonomies->map(function ($taxonomy, $site) { - // We need to set the site so that we can get to correct URL of the taxonomy. + $hreflang = $sites->map(function ($site) { + // Set the site so we can get the localized absolute URLs of the taxonomy. Site::setCurrent($site); return [ + 'href' => $this->absoluteUrl($this->taxonomy), 'hreflang' => Helpers::parseLocale(Site::current()->locale()), - 'href' => $this->absoluteUrl($taxonomy), ]; }); - // We need to set the site to the taxonomy origin site so that we can get to correct URL of the taxonomy. - Site::setCurrent($this->taxonomy->sites()->first()); + $originSite = $this->taxonomy->sites()->first(); - return $hreflang->put('x-default', [ - 'hreflang' => 'x-default', + $xDefaultSite = $sites->contains($originSite) ? $originSite : $this->site; + + // Set the site so we can get the localized absolute URL for the x-default. + Site::setCurrent($xDefaultSite); + + return $hreflang->push([ 'href' => $this->absoluteUrl($this->taxonomy), - ])->toArray(); + 'hreflang' => 'x-default', + ])->values()->all(); } public function lastmod(): string diff --git a/src/Sitemap/TermSitemapUrl.php b/src/Sitemap/TermSitemapUrl.php index 9f63f58c..b6813497 100644 --- a/src/Sitemap/TermSitemapUrl.php +++ b/src/Sitemap/TermSitemapUrl.php @@ -2,6 +2,7 @@ namespace Aerni\AdvancedSeo\Sitemap; +use Aerni\AdvancedSeo\Actions\Indexable; use Aerni\AdvancedSeo\Support\Helpers; use Illuminate\Support\Collection; use Statamic\Contracts\Taxonomies\Term; @@ -18,22 +19,29 @@ public function loc(): string public function alternates(): ?array { + if (! Site::multiEnabled()) { + return null; + } + $terms = $this->terms(); - // We only want alternate URLs if there are at least two terms. - if ($terms->count() <= 1) { + if ($terms->count() < 2) { return null; } - return $terms->map(fn ($term) => [ - 'hreflang' => Helpers::parseLocale(Site::get($term->locale())->locale()), + $hreflang = $terms->map(fn ($term) => [ 'href' => $this->absoluteUrl($term), - ]) - ->put('x-default', [ - 'hreflang' => 'x-default', - 'href' => $this->absoluteUrl($this->term->origin()), - ]) - ->toArray(); + 'hreflang' => Helpers::parseLocale($term->site()->locale()), + ]); + + $origin = $this->term->origin(); + + $xDefault = Indexable::handle($origin) ? $origin : $this->term; + + return $hreflang->push([ + 'href' => $this->absoluteUrl($xDefault), + 'hreflang' => 'x-default', + ])->values()->all(); } public function lastmod(): string @@ -67,7 +75,8 @@ public function isCanonicalUrl(): bool protected function terms(): Collection { - return $this->sitemap->terms($this->term->taxonomy()) + return $this->sitemap + ->terms($this->term->taxonomy()) ->filter(fn ($term) => $term->id() === $this->term->id()); } } diff --git a/src/View/Concerns/EvaluatesContextType.php b/src/View/Concerns/EvaluatesContextType.php new file mode 100644 index 00000000..46766ff3 --- /dev/null +++ b/src/View/Concerns/EvaluatesContextType.php @@ -0,0 +1,26 @@ +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/EvaluatesIndexability.php b/src/View/Concerns/EvaluatesIndexability.php new file mode 100644 index 00000000..09672771 --- /dev/null +++ b/src/View/Concerns/EvaluatesIndexability.php @@ -0,0 +1,53 @@ + $this->isIndexableContext($model), + $model instanceof Entry => $this->isIndexableEntryOrTerm($model), + $model instanceof LocalizedTerm => $this->isIndexableEntryOrTerm($model), + $model instanceof Site => $this->isIndexableSite($model->handle()), + }; + } + + protected function isIndexableContext(Context $context): bool + { + $model = $this->contextIsEntryOrTerm() + ? $context->get('id')->augmentable() + : $context->get('site'); + + return $this->isIndexable($model); + } + + protected function isIndexableEntryOrTerm(Entry|LocalizedTerm $model): bool + { + return $this->isIndexableSite($model->locale()) // If the site is not indexable, the model should not be indexed. + && $model->published() // Unpublished models should not be indexed. + && $model->url() // Models without a route should not be indexed. + && ! $model->seo_noindex; // Models with noindex should not be indexed. + } + + protected function isIndexableSite(string $locale): bool + { + if (! $this->crawlingIsEnabled()) { + return false; + } + + return ! Seo::find('site', 'indexing')?->in($locale)?->noindex; + } + + protected function crawlingIsEnabled(): bool + { + return in_array(app()->environment(), config('advanced-seo.crawling.environments', [])); + } +} diff --git a/src/View/Concerns/HasAbsoluteUrl.php b/src/View/Concerns/HasAbsoluteUrl.php new file mode 100644 index 00000000..899e7861 --- /dev/null +++ b/src/View/Concerns/HasAbsoluteUrl.php @@ -0,0 +1,17 @@ +baseUrl()) { + return URL::assemble($this->baseUrl(), $model->url()); + } + + return $model->absoluteUrl(); + } +} diff --git a/src/View/Concerns/HasHreflang.php b/src/View/Concerns/HasHreflang.php new file mode 100644 index 00000000..6203667f --- /dev/null +++ b/src/View/Concerns/HasHreflang.php @@ -0,0 +1,153 @@ +shouldIncludeHreflang($model)) { + return null; + } + + $sites = $model instanceof Entry + ? $model->sites() + : $model->taxonomy()->sites(); + + if ($sites->count() < 2) { + return null; + } + + $hreflang = $sites + ->map(fn ($locale) => $model->in($locale)) + ->filter() // A model might not exist in a site. So we need to remove it to prevent calling methods on null + ->filter($this->shouldIncludeHreflang(...)); + + if ($hreflang->count() < 2) { + return null; + } + + $hreflang->transform(fn ($model) => [ + 'url' => $this->absoluteUrl($model), + 'locale' => Helpers::parseLocale($model->site()->locale()), + ]); + + $origin = $model->origin() ?? $model; + + $xDefault = $this->shouldIncludeHreflang($origin) ? $origin : $model; + + return $hreflang->push([ + 'url' => $this->absoluteUrl($xDefault), + 'locale' => 'x-default', + ])->values()->all(); + } + + protected function taxonomyHreflang(Taxonomy $taxonomy): ?array + { + // Save initial site so that we can restore it later. + $initialSite = Site::current()->handle(); + + if (! $this->isIndexableSite($initialSite)) { + return null; + } + + $sites = $taxonomy + ->sites() + ->filter($this->isIndexableSite(...)); + + if ($sites->count() < 2) { + return null; + } + + $hreflang = $sites->map(function ($site) use ($taxonomy) { + // Set the site so we can get the localized absolute URLs of the taxonomy. + Site::setCurrent($site); + + return [ + 'url' => $taxonomy->absoluteUrl(), + 'locale' => Helpers::parseLocale(Site::current()->locale()), + ]; + }); + + $originSite = $taxonomy->sites()->first(); + + $xDefaultSite = $sites->contains($originSite) ? $originSite : $initialSite; + + // Set the site so we can get the localized absolute URL for the x-default. + Site::setCurrent($xDefaultSite); + + $hreflang->push([ + 'url' => $taxonomy->absoluteUrl(), + 'locale' => 'x-default', + ]); + + // Reset the site to the site of the model. + Site::setCurrent($initialSite); + + return $hreflang->values()->all(); + } + + protected function collectionTaxonomyHreflang(Taxonomy $taxonomy): ?array + { + $initialSite = Site::current()->handle(); + + if (! $this->isIndexableSite($initialSite)) { + return null; + } + + $sites = $taxonomy + ->sites() + ->filter($this->isIndexableSite(...)); + + if ($sites->count() < 2) { + return null; + } + + $hreflang = $sites->map(fn ($site) => [ + 'url' => $this->collectionTaxonomyUrl($taxonomy, $site), + 'locale' => Helpers::parseLocale(Site::get($site)->locale()), + ]); + + $originSite = $taxonomy->sites()->first(); + + $xDefaultSite = $sites->contains($originSite) ? $originSite : $initialSite; + + return $hreflang->push([ + 'url' => $this->collectionTaxonomyUrl($taxonomy, $xDefaultSite), + 'locale' => 'x-default', + ])->values()->all(); + } + + protected function collectionTaxonomyUrl(Taxonomy $taxonomy, string $site): string + { + $siteUrl = Site::get($site)->absoluteUrl(); + $taxonomyHandle = $taxonomy->handle(); + $collectionHandle = $taxonomy->collection()->handle(); + + return URL::tidy("{$siteUrl}/{$collectionHandle}/{$taxonomyHandle}"); + } + + protected function shouldIncludeHreflang(Entry|LocalizedTerm $model): bool + { + if ($this->canonicalPointsToAnotherUrl($model)) { + return false; + } + + return $this->isIndexable($model); + } + + protected function canonicalPointsToAnotherUrl(Entry|LocalizedTerm $model): bool + { + return $model->seo_canonical_type != 'current'; + } +} diff --git a/src/View/GraphQlCascade.php b/src/View/GraphQlCascade.php index 0c907fa8..0f9b5368 100644 --- a/src/View/GraphQlCascade.php +++ b/src/View/GraphQlCascade.php @@ -6,18 +6,26 @@ 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; use Spatie\SchemaOrg\Schema; use Statamic\Contracts\Assets\Asset; use Statamic\Contracts\Entries\Entry; use Statamic\Contracts\Taxonomies\Term; use Statamic\Facades\Data; +use Statamic\Facades\Site; use Statamic\Facades\URL; use Statamic\Support\Str; class GraphQlCascade extends BaseCascade { - use HasBaseUrl, HasComputedData; + use EvaluatesIndexability; + use HasAbsoluteUrl; + use HasBaseUrl; + use HasComputedData; + use HasHreflang; public function __construct(Entry|Term $model) { @@ -150,36 +158,21 @@ public function locale(): string return Helpers::parseLocale($this->model->site()->locale()); } - public function hreflang(): array + public function hreflang(): ?array { - $sites = $this->model instanceof Entry - ? $this->model->sites() - : $this->model->taxonomy()->sites(); - - $origin = $this->model instanceof Entry - ? $this->model->origin() ?? $this->model - : $this->model->inDefaultLocale(); - - $hreflang = $sites->map(fn ($locale) => $this->model->in($locale)) - ->filter() // A model might not exist in a site. So we need to remove it to prevent calling methods on null - ->filter(fn ($model) => $model->published()) // Remove any unpublished entries/terms - ->filter(fn ($model) => $model->url()) // Remove any entries/terms with no route - ->map(fn ($model) => [ - 'url' => $this->absoluteUrl($model), - 'locale' => Helpers::parseLocale($model->site()->locale()), - ]) - ->push([ - 'url' => $origin->published() ? $this->absoluteUrl($origin) : $this->absoluteUrl($this->model), - 'locale' => 'x-default', - ]) - ->values() - ->all(); + if (! Site::multiEnabled()) { + return null; + } - return $hreflang; + return $this->entryAndTermHreflang($this->model); } public function canonical(): ?string { + if (! $this->isIndexable($this->model)) { + return null; + } + $type = $this->get('canonical_type'); if ($type == 'other' && $this->has('canonical_entry')) { @@ -274,11 +267,4 @@ protected function breadcrumbsListItems(): Collection return $crumbs; } - - protected function absoluteUrl(mixed $model): ?string - { - return $this->baseUrl() - ? URL::assemble($this->baseUrl(), $model->url()) - : $model->absoluteUrl(); - } } diff --git a/src/View/ViewCascade.php b/src/View/ViewCascade.php index 472196a4..7099994f 100644 --- a/src/View/ViewCascade.php +++ b/src/View/ViewCascade.php @@ -6,11 +6,12 @@ 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; use Statamic\Contracts\Assets\Asset; -use Statamic\Contracts\Entries\Entry; -use Statamic\Contracts\Taxonomies\Taxonomy; use Statamic\Facades\Blink; use Statamic\Facades\Data; use Statamic\Facades\Site; @@ -21,7 +22,10 @@ class ViewCascade extends BaseCascade { + use EvaluatesContextType; + use EvaluatesIndexability; use HasComputedData; + use HasHreflang; public function __construct(Context $model) { @@ -182,112 +186,24 @@ public function locale(): string public function hreflang(): ?array { - // Handles collection taxonomy page. - if ($this->model->has('segment_2') && $this->model->get('terms') instanceof TermQueryBuilder) { - $taxonomy = $this->model->get('page'); - - return $taxonomy->sites() - ->map(fn ($site) => [ - 'url' => $this->getCollectionTaxonomyUrl($taxonomy, $site), - 'locale' => Helpers::parseLocale(Site::get($site)->locale()), - ]) - ->push([ - 'url' => $this->getCollectionTaxonomyUrl($taxonomy, $taxonomy->sites()->first()), - 'locale' => 'x-default', - ]) - ->all(); - } - - // Handles collection term page. - if ($this->model->has('segment_3') && $this->model->value('is_term') === true) { - $localizedTerm = $this->model->get('page'); - - return $localizedTerm->taxonomy()->sites() - ->map(fn ($locale) => [ - 'url' => $localizedTerm->in($locale)->absoluteUrl(), - 'locale' => Helpers::parseLocale(Site::get($locale)->locale()), - ]) - ->push([ - 'url' => $localizedTerm->origin()->absoluteUrl(), - 'locale' => 'x-default', - ]) - ->all(); - } - - // Handles taxonomy page. - if ($this->model->has('segment_1') && $this->model->get('terms') instanceof TermQueryBuilder) { - $taxonomy = $this->model->get('page'); - - $initialSite = Site::current()->handle(); - - $hreflang = $taxonomy->sites()->map(function ($locale) use ($taxonomy) { - // Set the current site so we can get the localized absolute URLs of the taxonomy. - Site::setCurrent($locale); - - return [ - 'url' => $taxonomy->absoluteUrl(), - 'locale' => Helpers::parseLocale(Site::current()->locale()), - ]; - }); - - // We need to set the site to the taxonomy origin site so that we can get to correct URL of the taxonomy. - Site::setCurrent($taxonomy->sites()->first()); - - $hreflang->push([ - 'url' => $taxonomy->absoluteUrl(), - 'locale' => 'x-default', - ]); - - // Reset the site to the original. - Site::setCurrent($initialSite); - - return $hreflang->toArray(); - } - - // Handle entries and term page. - $data = Data::find($this->model->value('id')); - - if (! $data) { + if (! Site::multiEnabled()) { return null; } - $sites = $data instanceof Entry - ? $data->sites() - : $data->taxonomy()->sites(); - - $origin = $data instanceof Entry - ? $data->origin() ?? $data - : $data->inDefaultLocale(); - - $hreflang = $sites->map(fn ($locale) => $data->in($locale)) - ->filter() // A model might not exist in a site. So we need to remove it to prevent calling methods on null - ->filter(fn ($model) => $model->published()) // Remove any unpublished entries/terms - ->filter(fn ($model) => $model->url()) // Remove any entries/terms with no route - ->map(fn ($model) => [ - 'url' => $model->absoluteUrl(), - 'locale' => Helpers::parseLocale($model->site()->locale()), - ]) - ->push([ - 'url' => $origin->published() ? $origin->absoluteUrl() : $data->absoluteUrl(), - 'locale' => 'x-default', - ]) - ->values() - ->all(); - - return $hreflang; - } - - protected function getCollectionTaxonomyUrl(Taxonomy $taxonomy, string $site): string - { - $siteUrl = Site::get($site)->absoluteUrl(); - $taxonomyHandle = $taxonomy->handle(); - $collectionHandle = $taxonomy->collection()->handle(); - - return URL::tidy("{$siteUrl}/{$collectionHandle}/{$taxonomyHandle}"); + 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')), + default => null + }; } public function canonical(): ?string { + if (! $this->isIndexable($this->model)) { + return null; + } + $type = $this->get('canonical_type'); if ($type == 'other' && $this->get('canonical_entry')) { @@ -315,6 +231,10 @@ public function canonical(): ?string public function prevUrl(): ?string { + if (! $this->isIndexable($this->model)) { + return null; + } + if (! $paginator = Blink::get('tag-paginator')) { return null; } @@ -335,6 +255,10 @@ public function prevUrl(): ?string public function nextUrl(): ?string { + if (! $this->isIndexable($this->model)) { + return null; + } + if (! $paginator = Blink::get('tag-paginator')) { return null; }