From e7e553089d8a3c8ce3a44aa557feed56087b1fc6 Mon Sep 17 00:00:00 2001 From: Kevin Meijer Date: Tue, 14 Jul 2026 09:58:38 +0200 Subject: [PATCH] Added SEO Pro support --- .../AvailableVariablesFieldtype.php | 64 ++++- .../AvailableVariablesFieldtypeTest.php | 240 ++++++++++++++++++ 2 files changed, 302 insertions(+), 2 deletions(-) diff --git a/src/Fieldtypes/AvailableVariablesFieldtype.php b/src/Fieldtypes/AvailableVariablesFieldtype.php index fb95f5c..9922d56 100644 --- a/src/Fieldtypes/AvailableVariablesFieldtype.php +++ b/src/Fieldtypes/AvailableVariablesFieldtype.php @@ -11,6 +11,7 @@ use Statamic\Fields\Blueprint; use Statamic\Fields\Fieldtype; use Statamic\Globals\GlobalSet as StatamicGlobalSet; +use Statamic\SeoPro\Cascade; use Statamic\Taxonomies\Taxonomy; class AvailableVariablesFieldtype extends Fieldtype @@ -104,7 +105,11 @@ protected function getEntryFields(): array $baseFields = [['name' => 'absolute_url', 'description' => 'Full URL']]; - return array_merge($baseFields, $collectionFields); + return array_merge( + $baseFields, + $collectionFields, + $this->getSeoProVariablesForSection($collection) + ); } /** @return array> */ @@ -140,7 +145,62 @@ protected function getTermFields(): array $baseFields = [['name' => 'absolute_url', 'description' => 'Full URL']]; - return array_merge($baseFields, $collectionFields); + return array_merge( + $baseFields, + $collectionFields, + $this->getSeoProVariablesForSection($taxonomy) + ); + } + + protected function seoProIsInstalled(): bool + { + return class_exists(Cascade::class); + } + + protected function seoIsEnabledForSection(Collection|Taxonomy $section): bool + { + if (! $this->seoProIsInstalled()) { + return false; + } + + return $section->cascade('seo') !== false; + } + + /** + * @return array> + */ + protected function getSeoProVariablesForSection(Collection|Taxonomy $section): array + { + if (! $this->seoIsEnabledForSection($section)) { + return []; + } + + return $this->getSeoProVariables(); + } + + /** + * @return array> + */ + protected function getSeoProVariables(): array + { + $variables = [ + 'title' => 'Meta Title', + 'compiled_title' => 'Compiled Title', + 'description' => 'Meta Description', + 'canonical_url' => 'Canonical URL', + 'og_title' => 'Open Graph Title', + 'og_type' => 'Open Graph Type', + 'image' => 'Open Graph Image', + ]; + + return collect($variables) + ->map(fn (string $description, string $handle): array => [ + 'name' => 'seo:'.$handle, + 'description' => $description, + 'children' => [], + ]) + ->values() + ->all(); } /** @return array */ diff --git a/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php b/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php index 72413a9..e2dbe42 100644 --- a/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php +++ b/tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php @@ -1104,4 +1104,244 @@ public function get_term_fields_includes_base_fields_and_maps_taxonomy_fields(): $fieldNames = array_column($result, 'name'); $this->assertContains('title', $fieldNames); } + + #[Test] + public function get_seo_pro_variables_returns_core_cascaded_fields(): void + { + $fieldtype = new AvailableVariablesFieldtype; + + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('getSeoProVariables'); + $method->setAccessible(true); + + $result = $method->invoke($fieldtype); + + $this->assertIsArray($result); + /** @var array> $result */ + $this->assertCount(7, $result); + $names = array_column($result, 'name'); + $this->assertSame([ + 'seo:title', + 'seo:compiled_title', + 'seo:description', + 'seo:canonical_url', + 'seo:og_title', + 'seo:og_type', + 'seo:image', + ], $names); + } + + #[Test] + public function get_entry_fields_appends_seo_pro_variables_when_seo_pro_is_available(): void + { + $fieldtype = new class extends AvailableVariablesFieldtype + { + protected function seoProIsInstalled(): bool + { + return true; + } + }; + + $collection = CollectionFacade::make('test'); + $collection->save(); + $blueprint = BlueprintFacade::make('test'); + $blueprint->setNamespace('collections.test'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + ], + ]); + $blueprint->save(); + $collection->entryBlueprints(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_collection = $collection; + $entryMock = $entry; + + /** @var Field $fieldMock */ + $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { + $mock->shouldReceive('parent')->andReturn($entryMock); + }); + + $fieldtype->setField($fieldMock); + + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('getEntryFields'); + $method->setAccessible(true); + + $result = $method->invoke($fieldtype); + + $this->assertIsArray($result); + /** @var array> $result */ + $fieldNames = array_column($result, 'name'); + + $this->assertContains('seo:compiled_title', $fieldNames); + + $titleIndex = array_search('title', $fieldNames, true); + $seoTitleIndex = array_search('seo:title', $fieldNames, true); + + $this->assertNotFalse($titleIndex); + $this->assertNotFalse($seoTitleIndex); + $this->assertGreaterThan($titleIndex, $seoTitleIndex); + } + + #[Test] + public function get_entry_fields_omits_seo_pro_variables_when_seo_is_disabled_for_collection(): void + { + $fieldtype = new class extends AvailableVariablesFieldtype + { + protected function seoProIsInstalled(): bool + { + return true; + } + }; + + $collection = CollectionFacade::make('test'); + $collection->cascade(['seo' => false]); + $collection->save(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_collection = $collection; + $entryMock = $entry; + + /** @var Field $fieldMock */ + $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { + $mock->shouldReceive('parent')->andReturn($entryMock); + }); + + $fieldtype->setField($fieldMock); + + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('getEntryFields'); + $method->setAccessible(true); + + $result = $method->invoke($fieldtype); + + $this->assertIsArray($result); + /** @var array> $result */ + $fieldNames = array_column($result, 'name'); + + $this->assertNotContains('seo:title', $fieldNames); + } + + #[Test] + public function get_term_fields_appends_seo_pro_variables_when_seo_pro_is_available(): void + { + $fieldtype = new class extends AvailableVariablesFieldtype + { + protected function seoProIsInstalled(): bool + { + return true; + } + }; + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('test'); + $taxonomy->save(); + $blueprint = BlueprintFacade::make('test'); + $blueprint->setNamespace('taxonomies.test'); + $blueprint->setContents([ + 'fields' => [ + [ + 'handle' => 'title', + 'field' => [ + 'type' => 'text', + 'display' => 'Title', + ], + ], + ], + ]); + $blueprint->save(); + /** @phpstan-ignore-next-line */ + $taxonomy->termBlueprints(['test']); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_taxonomy = $taxonomy; + $entryMock = $entry; + + /** @var Field $fieldMock */ + $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { + $mock->shouldReceive('parent')->andReturn($entryMock); + }); + + $fieldtype->setField($fieldMock); + + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('getTermFields'); + $method->setAccessible(true); + + $result = $method->invoke($fieldtype); + + $this->assertIsArray($result); + /** @var array> $result */ + $fieldNames = array_column($result, 'name'); + + $this->assertContains('seo:description', $fieldNames); + } + + #[Test] + public function get_term_fields_omits_seo_pro_variables_when_seo_is_disabled_for_taxonomy(): void + { + $fieldtype = new class extends AvailableVariablesFieldtype + { + protected function seoProIsInstalled(): bool + { + return true; + } + }; + + /** @var Taxonomy $taxonomy */ + $taxonomy = TaxonomyFacade::make('test'); + $taxonomy->cascade(['seo' => false]); + $taxonomy->save(); + + $templatesCollection = CollectionFacade::make('structured_data_templates'); + $templatesCollection->save(); + $entry = (new Entry) + ->collection($templatesCollection) + ->id('template-123'); + + $entry->use_for_taxonomy = $taxonomy; + $entryMock = $entry; + + /** @var Field $fieldMock */ + $fieldMock = $this->mock(Field::class, function (MockInterface $mock) use ($entryMock): void { + $mock->shouldReceive('parent')->andReturn($entryMock); + }); + + $fieldtype->setField($fieldMock); + + $reflection = new \ReflectionClass($fieldtype); + $method = $reflection->getMethod('getTermFields'); + $method->setAccessible(true); + + $result = $method->invoke($fieldtype); + + $this->assertIsArray($result); + /** @var array> $result */ + $fieldNames = array_column($result, 'name'); + + $this->assertNotContains('seo:title', $fieldNames); + } }