Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 62 additions & 2 deletions src/Fieldtypes/AvailableVariablesFieldtype.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<int, array<string, mixed>> */
Expand Down Expand Up @@ -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<int, array<string, mixed>>
*/
protected function getSeoProVariablesForSection(Collection|Taxonomy $section): array
{
if (! $this->seoIsEnabledForSection($section)) {
return [];
}

return $this->getSeoProVariables();
}

/**
* @return array<int, array<string, mixed>>
*/
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<int, mixed> */
Expand Down
240 changes: 240 additions & 0 deletions tests/Unit/Fieldtypes/AvailableVariablesFieldtypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, array<string, mixed>> $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<int, array<string, mixed>> $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<int, array<string, mixed>> $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<int, array<string, mixed>> $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<int, array<string, mixed>> $result */
$fieldNames = array_column($result, 'name');

$this->assertNotContains('seo:title', $fieldNames);
}
}
Loading