Add and enable the drupal_cms_seo_tools recipe

```bash
composer require drupal/drupal_cms_seo_tools
drush recipe $(pwd)/recipes/drupal_cms_seo_tools
```
This commit is contained in:
Oliver Davies 2025-04-16 18:18:17 +01:00
parent a4eacb5cf4
commit bc2ab546d4
17 changed files with 2547 additions and 1 deletions

View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\drupal_cms_seo_tools\Functional;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigInstallerInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\BrowserTestBase;
/**
* @group drupal_cms_seo_tools
*/
class ComponentValidationTest extends BrowserTestBase {
use RecipeTestTrait {
applyRecipe as traitApplyRecipe;
}
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a content type so we can test the changes made by the recipe.
$this->drupalCreateContentType(['type' => 'test'])->id();
}
private function applyRecipe(mixed ...$arguments): void {
$dir = realpath(__DIR__ . '/../../..');
$this->traitApplyRecipe($dir, ...$arguments);
}
public function test(): void {
// The recipe should apply cleanly.
$this->applyRecipe();
// Apply it again to prove that it is idempotent.
$this->applyRecipe();
// There should be an SEO image field on our test content type, referencing
// image media.
$field_settings = FieldConfig::loadByName('node', 'test', 'field_seo_image')?->getSettings();
$this->assertIsArray($field_settings);
$this->assertSame('default:media', $field_settings['handler']);
$this->assertContains('image', $field_settings['handler_settings']['target_bundles']);
// Check sitemap works as expected for anonymous users.
$this->checkSitemap();
// Check sitemap works as expected for authenticated users too.
$authenticated = $this->createUser();
$this->drupalLogin($authenticated);
$this->checkSitemap();
}
public function testAutomaticSitemapSettings(): void {
$this->applyRecipe();
// We should have Simple Sitemap settings for the extant content type.
$settings = $this->container->get('config.storage')
->listAll('simple_sitemap.bundle_settings');
$this->assertSame(['simple_sitemap.bundle_settings.default.node.test'], $settings);
$get_settings = function (string $node_type): Config {
return $this->config("simple_sitemap.bundle_settings.default.node.$node_type");
};
// If we create a new content type programmatically, Simple Sitemap settings
// should be generated for it automatically.
$node_type = $this->drupalCreateContentType()->id();
$this->assertFalse($get_settings($node_type)->isNew());
// If we create a new content type in the UI, Simple Sitemap settings should
// NOT be automatically generated.
$account = $this->createUser([
'administer content types',
'administer sitemap settings',
]);
$this->drupalLogin($account);
$this->drupalGet('/admin/structure/types/add');
$node_type = $this->randomMachineName();
$this->submitForm([
'name' => $node_type,
'type' => $node_type,
'simple_sitemap[default][index]' => 0,
], 'Save');
$this->assertTrue($get_settings($node_type)->isNew());
// Extant settings should not be changed...
$get_settings('test')->set('priority', '0.3')->save();
$this->assertSame('0.3', $get_settings('test')->get('priority'));
// ...even if we reapply the recipe...
$this->applyRecipe();
$this->assertSame('0.3', $get_settings('test')->get('priority'));
// ...or sync config (here, we are simulating that the priority was changed
// by a config sync).
$this->container->get(ConfigInstallerInterface::class)->setSyncing(TRUE);
$get_settings('test')->set('priority', '0.2')->save();
$this->assertSame('0.2', $get_settings('test')->get('priority'));
}
/**
* Checks that the sitemap is accessible and contains the expected links.
*/
private function checkSitemap(): void {
// Create a main menu link to ensure it shows up in the site map.
$node = $this->drupalCreateNode(['type' => 'test']);
$menu_link = MenuLinkContent::create([
'title' => $node->getTitle(),
'link' => 'internal:' . $node->toUrl()->toString(),
'menu_name' => 'main',
]);
$menu_link->save();
$this->drupalGet('/sitemap');
$assert_session = $this->assertSession();
$assert_session->statusCodeEquals(200);
$assert_session->linkByHrefNotExists('/rss.xml');
$site_map = $assert_session->elementExists('css', '.sitemap');
$site_name = $this->config('system.site')->get('name');
$this->assertTrue($site_map->hasLink("Front page of $site_name"), 'Front page link does not appear in the site map.');
$this->assertTrue($site_map->hasLink($menu_link->label()), 'Main menu links do not appear in the site map.');
}
}

View file

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\drupal_cms_seo_tools\Functional;
use Composer\InstalledVersions;
use Drupal\Core\Url;
use Drupal\file\Entity\File;
use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
use Drupal\image\Entity\ImageStyle;
use Drupal\media\Entity\Media;
use Drupal\Tests\BrowserTestBase;
/**
* @group drupal_cms_seo_tools
*/
class ContentMetaTagsTest extends BrowserTestBase {
use RecipeTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
private function generateImage(string $extension): Media {
$random = $this->getRandomGenerator();
$uri = uniqid('public://') . '.' . $extension;
$uri = $random->image($uri, '100x100', '200x200');
$this->assertFileExists($uri);
$file = File::create(['uri' => $uri]);
$file->save();
$media = Media::create([
'name' => $random->word(16),
'bundle' => 'image',
'field_media_image' => [
'target_id' => $file->id(),
'alt' => $random->machineName(),
],
]);
$media->save();
return $media;
}
/**
* @testWith ["drupal/drupal_cms_blog", "blog"]
* ["drupal/drupal_cms_case_study", "case_study"]
* ["drupal/drupal_cms_events", "event"]
* ["drupal/drupal_cms_news", "news"]
* ["drupal/drupal_cms_page", "page"]
* ["drupal/drupal_cms_person", "person"]
* ["drupal/drupal_cms_project", "project"]
*/
public function testMetaTagsForContentType(string $recipe, string $node_type): void {
$dir = InstalledVersions::getInstallPath($recipe);
$this->applyRecipe($dir);
$dir = realpath(__DIR__ . '/../../..');
$this->applyRecipe($dir);
// If we create a node of this content type, all expected meta tags should
// be there.
$random = $this->getRandomGenerator();
$node = $this->drupalCreateNode([
'type' => $node_type,
'field_featured_image' => $this->generateImage('png'),
'field_description' => $random->sentences(4),
'moderation_state' => 'published',
]);
$node_url = $node->toUrl();
$this->drupalGet($node_url);
$assert_session = $this->assertSession();
$assert_session->statusCodeEquals(200);
$save_node = function () use ($node): void {
$node->save();
$this->container->get('cache.page')->deleteAll();
$this->getSession()->reload();
};
// Assert the meta tags which are static, or don't have any configured
// overrides.
$absolute_node_url = $node_url->setAbsolute()->toString();
$assert_session->elementAttributeContains('css', 'link[rel="canonical"]', 'href', $absolute_node_url);
$site_name = $this->config('system.site')->get('name');
$assert_session->elementAttributeContains('css', 'meta[property="og:site_name"]', 'content', $site_name);
$assert_session->elementAttributeContains('css', 'meta[property="og:type"]', 'content', 'article');
$assert_session->elementAttributeContains('css', 'meta[property="og:url"]', 'content', $absolute_node_url);
$assert_session->elementAttributeContains('css', 'meta[name="referrer"]', 'content', 'unsafe-url');
$assert_session->elementAttributeContains('css', 'link[rel="shortlink"]', 'href', Url::fromRoute('<front>')->setAbsolute()->toString());
$assert_session->elementAttributeContains('css', 'meta[name="rights"]', 'content', sprintf('Copyright ©%s All rights reserved.', date('Y')));
$assert_session->elementAttributeContains('css', 'meta[name="twitter:card"]', 'content', 'summary_large_image');
$original_changed_time = $node->getChangedTime();
$assert_session->elementAttributeContains('css', 'meta[property="og:updated_time"]', 'content', date('c', $original_changed_time));
// Re-saving the node should update the og:updated_time meta tag.
$updated_changed_time = $original_changed_time + 30;
$node->setChangedTime($updated_changed_time);
$save_node();
$assert_session->elementAttributeContains('css', 'meta[property="og:updated_time"]', 'content', date('c', $updated_changed_time));
// Assert the meta tags for field_featured_image, and that field_seo_image
// takes precedence over it.
$assert_image = function (Media $image) use ($assert_session): void {
/** @var \Drupal\file\FileInterface $file */
$file = $image->field_media_image->entity;
$name = $file->getFilename();
$facebook_dimensions = [];
ImageStyle::load('social_media_facebook')
?->transformDimensions($facebook_dimensions, $file->getFileUri());
$this->assertArrayHasKey('width', $facebook_dimensions);
$this->assertArrayHasKey('height', $facebook_dimensions);
$assert_session->elementAttributeContains('css', 'link[rel="image_src"]', 'href', $name);
$assert_session->elementAttributeContains('css', 'meta[property="og:image"]', 'content', $name);
$alt_text = $image->field_media_image->alt;
$assert_session->elementAttributeContains('css', 'meta[property="og:image:alt"]', 'content', $alt_text);
$assert_session->elementAttributeContains('css', 'meta[property="og:image:width"]', 'content', (string) $facebook_dimensions['width']);
$assert_session->elementAttributeContains('css', 'meta[property="og:image:height"]', 'content', (string) $facebook_dimensions['height']);
$assert_session->elementAttributeContains('css', 'meta[property="og:image:type"]', 'content', 'image/webp');
$assert_session->elementAttributeContains('css', 'meta[name="twitter:image"]', 'content', $name);
$assert_session->elementAttributeContains('css', 'meta[name="twitter:image:alt"]', 'content', $alt_text);
};
$assert_image($node->field_featured_image->entity);
$node->set('field_seo_image', $this->generateImage('jpg'));
$save_node();
$assert_image($node->field_seo_image->entity);
// Assert the meta tags for the node title and that field_seo_title takes
// precedence over it.
$assert_title = function (string $title) use ($assert_session, $site_name): void {
$assert_session->elementAttributeContains('css', 'meta[property="og:title"]', 'content', $title);
$assert_session->elementAttributeContains('css', 'meta[name="twitter:title"]', 'content', $title);
$assert_session->titleEquals("$title | $site_name");
};
$assert_title($node->getTitle());
$seo_title = $this->randomMachineName();
$node->set('field_seo_title', $seo_title);
$save_node();
$assert_title($seo_title);
// Assert the meta tags for field_description and that field_seo_description
// takes precedence over it.
$assert_description = function (string $description) use ($assert_session): void {
$assert_session->elementAttributeContains('css', 'meta[name="description"]', 'content', $description);
$assert_session->elementAttributeContains('css', 'meta[property="og:description"]', 'content', $description);
$assert_session->elementAttributeContains('css', 'meta[name="twitter:description"]', 'content', $description);
};
$assert_description($node->field_description->value);
$node->set('field_seo_description', $random->sentences(4));
$save_node();
$assert_description($node->field_seo_description->value);
}
}