From bc2ab546d4b4c2b7cc2046acca9344ae6ad9288d Mon Sep 17 00:00:00 2001 From: Oliver Davies Date: Wed, 16 Apr 2025 18:18:17 +0100 Subject: [PATCH] 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 ``` --- composer.json | 1 + composer.lock | 1260 ++++++++++++++++- recipes/drupal_cms_seo_tools/LICENSE.txt | 339 +++++ recipes/drupal_cms_seo_tools/composer.json | 25 + .../config/eca.eca.node_sitemap_settings.yml | 200 +++ .../field.storage.node.field_seo_analysis.yml | 18 + ...eld.storage.node.field_seo_description.yml | 18 + .../field.storage.node.field_seo_image.yml | 19 + .../field.storage.node.field_seo_title.yml | 20 + .../image.style.social_media_facebook.yml | 22 + .../config/image.style.social_media_x.yml | 22 + .../config/metatag.metatag_defaults.front.yml | 28 + .../metatag.metatag_defaults.global.yml | 13 + .../config/metatag.metatag_defaults.node.yml | 28 + recipes/drupal_cms_seo_tools/recipe.yml | 236 +++ .../Functional/ComponentValidationTest.php | 139 ++ .../src/Functional/ContentMetaTagsTest.php | 160 +++ 17 files changed, 2547 insertions(+), 1 deletion(-) create mode 100644 recipes/drupal_cms_seo_tools/LICENSE.txt create mode 100644 recipes/drupal_cms_seo_tools/composer.json create mode 100644 recipes/drupal_cms_seo_tools/config/eca.eca.node_sitemap_settings.yml create mode 100644 recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_analysis.yml create mode 100644 recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_description.yml create mode 100644 recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_image.yml create mode 100644 recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_title.yml create mode 100644 recipes/drupal_cms_seo_tools/config/image.style.social_media_facebook.yml create mode 100644 recipes/drupal_cms_seo_tools/config/image.style.social_media_x.yml create mode 100644 recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.front.yml create mode 100644 recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.global.yml create mode 100644 recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.node.yml create mode 100644 recipes/drupal_cms_seo_tools/recipe.yml create mode 100644 recipes/drupal_cms_seo_tools/tests/src/Functional/ComponentValidationTest.php create mode 100644 recipes/drupal_cms_seo_tools/tests/src/Functional/ContentMetaTagsTest.php diff --git a/composer.json b/composer.json index 9ef7ec84e..e7310da56 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "drupal/core-composer-scaffold": "^11.1", "drupal/core-project-message": "^11.1", "drupal/core-recommended": "^11.1", + "drupal/drupal_cms_seo_tools": "^1.1", "drupal/tome": "^1.13", "drush/drush": "^13.5" }, diff --git a/composer.lock b/composer.lock index 911e6226b..5d8b692ee 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8fbea5c97e8c1dfb309532bf35f06476", + "content-hash": "2ec98af4b13fbee1322d2752140e6765", "packages": [ { "name": "asm89/stack-cors", @@ -1085,6 +1085,187 @@ ], "time": "2024-02-05T11:35:39+00:00" }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "8c784d071debd117328803d86b2097615b457500" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2024-10-09T13:47:03+00:00" + }, + { + "name": "drupal/bpmn_io", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/bpmn_io.git", + "reference": "2.0.8" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/bpmn_io-2.0.8.zip", + "reference": "2.0.8", + "shasum": "3fe8734f2a9570fcfdbe47167eafa1e3402f7c03" + }, + "require": { + "drupal/core": "^10.3 || ^11", + "drupal/eca": "^2.0", + "drupal/eca_modeller_bpmn": "^2", + "php": ">=8.1" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "2.0.8", + "datestamp": "1744192017", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "boromino", + "homepage": "https://www.drupal.org/user/859722" + }, + { + "name": "jurgenhaas", + "homepage": "https://www.drupal.org/user/168924" + } + ], + "description": "BPMN modeller for ECA, integrated into Drupal's admin UI.", + "homepage": "https://www.drupal.org/project/bpmn_io", + "support": { + "source": "https://drupal.org/project/bpmn_io", + "issues": "https://drupal.org/project/issues/bpmn_io" + } + }, + { + "name": "drupal/checklistapi", + "version": "2.1.6", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/checklistapi.git", + "reference": "2.1.6" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/checklistapi-2.1.6.zip", + "reference": "2.1.6", + "shasum": "64f439b7dd09336c4b32480edf1796cbc911d9b4" + }, + "require": { + "drupal/core": "^9.3 || ^10 || ^11", + "php": ">=7.3.0" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "2.1.6", + "datestamp": "1713801912", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": "^10" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Travis Carden", + "homepage": "https://www.drupal.org/user/3550392", + "email": "travis.carden@gmail.com" + }, + { + "name": "rajeshreeputra", + "homepage": "https://www.drupal.org/user/3418561" + }, + { + "name": "traviscarden", + "homepage": "https://www.drupal.org/user/236758" + }, + { + "name": "vladimiraus", + "homepage": "https://www.drupal.org/user/673120" + } + ], + "description": "Provides an API for creating fillable, persistent checklists.", + "homepage": "http://drupal.org/project/checklistapi", + "support": { + "source": "https://git.drupalcode.org/project/checklistapi", + "issues": "https://www.drupal.org/project/issues/checklistapi" + } + }, { "name": "drupal/core", "version": "11.1.6", @@ -1415,6 +1596,864 @@ ], "description": "Core and its dependencies with known-compatible minor versions. Require this project INSTEAD OF drupal/core." }, + { + "name": "drupal/crop", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/crop.git", + "reference": "8.x-2.4" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/crop-8.x-2.4.zip", + "reference": "8.x-2.4", + "shasum": "be11fad0abf1d53544d35cb4ca6cedd8e91d2542" + }, + "require": { + "drupal/core": "^9.3 || ^10 || ^11" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-2.4", + "datestamp": "1720455738", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Drupal Media Team", + "homepage": "https://www.drupal.org/user/3260690" + }, + { + "name": "phenaproxima", + "homepage": "https://www.drupal.org/user/205645" + }, + { + "name": "slashrsm", + "homepage": "https://www.drupal.org/user/744628" + }, + { + "name": "woprrr", + "homepage": "https://www.drupal.org/user/858604" + } + ], + "description": "Provides storage and API for image crops.", + "homepage": "https://www.drupal.org/project/crop", + "support": { + "source": "https://git.drupalcode.org/project/crop", + "issues": "https://www.drupal.org/project/issues/crop" + } + }, + { + "name": "drupal/drupal_cms_seo_tools", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/drupal_cms_seo_tools.git", + "reference": "1842600f199e159e44042a981001c40c59a62642" + }, + "dist": { + "type": "zip", + "url": "https://git.drupalcode.org/api/v4/projects/project%2Fdrupal_cms_seo_tools/repository/archive.zip?sha=1842600f199e159e44042a981001c40c59a62642", + "reference": "1842600f199e159e44042a981001c40c59a62642", + "shasum": "" + }, + "require": { + "drupal/bpmn_io": "^2.0.6", + "drupal/core": ">=10.4", + "drupal/eca": "^2.1.4", + "drupal/field_group": "^3.6", + "drupal/focal_point": "^2.1", + "drupal/metatag": "^2", + "drupal/robotstxt": "^1.6", + "drupal/seo_checklist": "^5.2.1", + "drupal/simple_sitemap": "^4.2.2", + "drupal/sitemap": "^2", + "drupal/token_or": "^2.2", + "drupal/yoast_seo": "^2.1" + }, + "suggest": { + "drupal/drupal_cms_accessibility_tools": "Adds automated checks to help ensure content conforms to web accessibility guidelines.", + "drupal/drupal_cms_analytics": "Adds recipes for tracking website traffic." + }, + "type": "drupal-recipe", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Adds common search engine optimisation tools such as meta tags, XML sitemap, robots.txt management and more.", + "support": { + "source": "https://git.drupalcode.org/project/drupal_cms_seo_tools/-/tree/1.1.0" + }, + "time": "2025-03-18T15:59:45+00:00" + }, + { + "name": "drupal/eca", + "version": "2.1.7", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/eca.git", + "reference": "2.1.7" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/eca-2.1.7.zip", + "reference": "2.1.7", + "shasum": "8a9c6d2c02952c1c92f2189d3665fc11d49eca43" + }, + "require": { + "dragonmantank/cron-expression": "^3.1", + "drupal/core": "^10.3||^11", + "ext-dom": "*", + "ext-json": "*", + "mtownsend/xml-to-array": "^2.0", + "php": ">=8.1" + }, + "require-dev": { + "drupal/eca_ui": "*", + "drupal/entity_reference_revisions": "^1.12", + "drupal/inline_entity_form": "^3.0", + "drupal/paragraphs": "^1.18", + "drupal/project_browser": "^2.0", + "drupal/token": "^1.15", + "drupal/webform": "dev-3465838-drupal-11-compatibility" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "2.1.7", + "datestamp": "1744222322", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "boromino", + "homepage": "https://www.drupal.org/user/859722" + }, + { + "name": "danielspeicher", + "homepage": "https://www.drupal.org/user/3621778" + }, + { + "name": "jurgenhaas", + "homepage": "https://www.drupal.org/user/168924" + } + ], + "description": "Event, Conditions, Actions - powerful, versatile, and user-friendly rules engine for Drupal", + "homepage": "https://www.drupal.org/project/eca", + "support": { + "source": "https://drupal.org/project/eca", + "issues": "https://drupal.org/project/issues/eca" + } + }, + { + "name": "drupal/eca_modeller_bpmn", + "version": "2.1.7", + "require": { + "drupal/core": "^10.3 || ^11", + "drupal/eca": "^2", + "drupal/eca_ui": "*" + }, + "type": "metapackage", + "extra": { + "drupal": { + "version": "2.1.7", + "datestamp": "1744222322", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "boromino", + "homepage": "https://www.drupal.org/user/859722" + }, + { + "name": "danielspeicher", + "homepage": "https://www.drupal.org/user/3621778" + }, + { + "name": "jurgenhaas", + "homepage": "https://www.drupal.org/user/168924" + } + ], + "description": "Common functionality for all BPMN based modeller implementations.", + "homepage": "https://www.drupal.org/project/eca", + "support": { + "source": "https://git.drupalcode.org/project/eca" + } + }, + { + "name": "drupal/eca_ui", + "version": "2.1.7", + "require": { + "drupal/core": "^10.3 || ^11", + "drupal/eca": "*" + }, + "type": "metapackage", + "extra": { + "drupal": { + "version": "2.1.7", + "datestamp": "1744222322", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "boromino", + "homepage": "https://www.drupal.org/user/859722" + }, + { + "name": "danielspeicher", + "homepage": "https://www.drupal.org/user/3621778" + }, + { + "name": "jurgenhaas", + "homepage": "https://www.drupal.org/user/168924" + } + ], + "description": "Provides a user interface for managing ECA models.", + "homepage": "https://www.drupal.org/project/eca", + "support": { + "source": "https://git.drupalcode.org/project/eca" + } + }, + { + "name": "drupal/field_group", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/field_group.git", + "reference": "8.x-3.6" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/field_group-8.x-3.6.zip", + "reference": "8.x-3.6", + "shasum": "427c0a65dc1936e69e60c120776056cfe5b43e86" + }, + "require": { + "drupal/core": "^9.2 || ^10 || ^11" + }, + "require-dev": { + "drupal/jquery_ui_accordion": "*" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-3.6", + "datestamp": "1722672510", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "anybody", + "homepage": "https://www.drupal.org/user/291091" + }, + { + "name": "grevil", + "homepage": "https://www.drupal.org/user/3668491" + }, + { + "name": "hydra", + "homepage": "https://www.drupal.org/user/647364" + }, + { + "name": "joevagyok", + "homepage": "https://www.drupal.org/user/2876343" + }, + { + "name": "jyve", + "homepage": "https://www.drupal.org/user/591438" + }, + { + "name": "nils.destoop", + "homepage": "https://www.drupal.org/user/361625" + }, + { + "name": "Stalski", + "homepage": "https://www.drupal.org/user/322618" + }, + { + "name": "swentel", + "homepage": "https://www.drupal.org/user/107403" + } + ], + "description": "Provides the field_group module.", + "homepage": "https://www.drupal.org/project/field_group", + "support": { + "source": "https://git.drupalcode.org/project/field_group", + "issues": "https://www.drupal.org/project/issues/field_group" + } + }, + { + "name": "drupal/focal_point", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/focal_point.git", + "reference": "2.1.2" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/focal_point-2.1.2.zip", + "reference": "2.1.2", + "shasum": "5f8ffadd37748506c8f00314b1d45c947eb27cf7" + }, + "require": { + "drupal/core": "^9.3 || ^10 || ^11", + "drupal/crop": "^2.3" + }, + "require-dev": { + "drupal/crop": "*" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "2.1.2", + "datestamp": "1731556344", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Alexander Ross (bleen)", + "homepage": "https://www.drupal.org/u/bleen", + "role": "Maintainer" + }, + { + "name": "rajeshreeputra", + "homepage": "https://www.drupal.org/user/3418561" + } + ], + "description": "Focal Point allows content creators to mark the most important part of an image for easier cropping.", + "homepage": "https://drupal.org/project/focal_point", + "support": { + "source": "https://cgit.drupalcode.org/focal_point", + "issues": "https://drupal.org/project/issues/focal_point", + "irc": "irc://irc.freenode.org/drupal-contribute" + } + }, + { + "name": "drupal/metatag", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/metatag.git", + "reference": "2.1.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/metatag-2.1.0.zip", + "reference": "2.1.0", + "shasum": "c28fe2fdac68a9370a6af6cbafff4425dd5148f3" + }, + "require": { + "drupal/core": "^9.4 || ^10 || ^11", + "drupal/token": "^1.0", + "php": ">=8.0" + }, + "require-dev": { + "drupal/forum": "*", + "drupal/hal": "^1 || ^2 || ^9", + "drupal/metatag_dc": "*", + "drupal/metatag_open_graph": "*", + "drupal/page_manager": "^4.0", + "drupal/redirect": "^1.0", + "ergebnis/composer-normalize": "*", + "mpyw/phpunit-patch-serializable-comparison": "*" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "2.1.0", + "datestamp": "1731004042", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "See contributors", + "homepage": "https://www.drupal.org/node/640498/committers", + "role": "Developer" + }, + { + "name": "dave reid", + "homepage": "https://www.drupal.org/user/53892" + } + ], + "description": "Manage meta tags for all entities.", + "homepage": "https://www.drupal.org/project/metatag", + "keywords": [ + "Drupal", + "seo" + ], + "support": { + "source": "https://git.drupalcode.org/project/metatag", + "issues": "https://www.drupal.org/project/issues/metatag", + "docs": "https://www.drupal.org/docs/8/modules/metatag" + } + }, + { + "name": "drupal/robotstxt", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/robotstxt.git", + "reference": "8.x-1.6" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/robotstxt-8.x-1.6.zip", + "reference": "8.x-1.6", + "shasum": "157192b2c6b2cc9779336cb3a5e752ffa2409c6b" + }, + "require": { + "drupal/core": "^9.3 || ^10 || ^11" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-1.6", + "datestamp": "1723570930", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "hass", + "homepage": "https://www.drupal.org/u/hass" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/53579/committers" + }, + { + "name": "generalredneck", + "homepage": "https://www.drupal.org/user/368854" + }, + { + "name": "kevinquillen", + "homepage": "https://www.drupal.org/user/317279" + }, + { + "name": "mikeegoulding", + "homepage": "https://www.drupal.org/user/2867877" + }, + { + "name": "Todd Nienkerk", + "homepage": "https://www.drupal.org/user/92096" + } + ], + "description": "Generates the robots.txt file dynamically and gives you the chance to edit it, on a per-site basis, from the web UI.", + "homepage": "https://www.drupal.org/project/robotstxt", + "support": { + "source": "https://git.drupal.org/project/robotstxt.git", + "issues": "https://www.drupal.org/project/issues/robotstxt" + } + }, + { + "name": "drupal/seo_checklist", + "version": "5.2.4", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/seo_checklist.git", + "reference": "5.2.4" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/seo_checklist-5.2.4.zip", + "reference": "5.2.4", + "shasum": "d50daa43114354755adbe449f9a948c9ce16cfae" + }, + "require": { + "drupal/checklistapi": "^2.0", + "drupal/core": "^8.8 || ^9 || ^10 || ^11" + }, + "require-dev": { + "drupal/addtoany": "*", + "drupal/admin_toolbar": "*", + "drupal/advagg": "*", + "drupal/cloudflare": "*", + "drupal/coffee": "*", + "drupal/diff": "*", + "drupal/easy_breadcrumb": "*", + "drupal/editor_advanced_link": "*", + "drupal/filefield_paths": "*", + "drupal/google_tag": "*", + "drupal/honeypot": "*", + "drupal/hreflang": "*", + "drupal/metatag": "*", + "drupal/pathauto": "*", + "drupal/redirect": "*", + "drupal/robotstxt": "*", + "drupal/scheduler": "*", + "drupal/search404": "*", + "drupal/security_review": "*", + "drupal/sitemap": "*", + "drupal/xmlsitemap": "*", + "drupal/yoast_seo": "*" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "5.2.4", + "datestamp": "1736549430", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "ben finklea", + "homepage": "https://www.drupal.org/user/46676" + }, + { + "name": "thejimbirch", + "homepage": "https://www.drupal.org/user/2507260" + }, + { + "name": "traviscarden", + "homepage": "https://www.drupal.org/user/236758" + } + ], + "description": "Uses best practices to check for proper search engine optimization.", + "homepage": "https://www.drupal.org/project/seo_checklist", + "keywords": [ + "SEO" + ], + "support": { + "source": "http://drupalcode.org/seo_checklist", + "issues": "https://www.drupal.org/project/issues/seo_checklist" + } + }, + { + "name": "drupal/simple_sitemap", + "version": "4.2.2", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/simple_sitemap.git", + "reference": "4.2.2" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/simple_sitemap-4.2.2.zip", + "reference": "4.2.2", + "shasum": "1f9c9197d37450fb347a4fa3f10191f5f4b5ef13" + }, + "require": { + "drupal/core": "^10.2 || ^11", + "ext-xmlwriter": "*" + }, + "conflict": { + "drush/drush": "<12.5.1" + }, + "require-dev": { + "drupal/paragraphs": "^1.18" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "4.2.2", + "datestamp": "1732485885", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Pawel Ginalski (gbyte)", + "homepage": "https://www.drupal.org/u/gbyte", + "email": "contact@gbyte.dev", + "role": "Maintainer" + }, + { + "name": "walkingdexter", + "homepage": "https://www.drupal.org/user/3251330" + } + ], + "description": "Creates a standard conform hreflang XML sitemap of the site content and provides a framework for developing other sitemap types.", + "homepage": "https://drupal.org/project/simple_sitemap", + "support": { + "source": "https://cgit.drupalcode.org/simple_sitemap", + "issues": "https://drupal.org/project/issues/simple_sitemap" + } + }, + { + "name": "drupal/sitemap", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/sitemap.git", + "reference": "8.x-2.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/sitemap-8.x-2.0.zip", + "reference": "8.x-2.0", + "shasum": "1e1db0da34ff07318cd2179b392d6495da49a0f4" + }, + "require": { + "drupal/core": "^10.2 || ^11" + }, + "require-dev": { + "drupal/book": "*" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-2.0", + "datestamp": "1724850275", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Nik Alexandrov", + "homepage": "https://www.drupal.org/u/nafes", + "role": "Maintainer" + }, + { + "name": "Anna Kalata", + "homepage": "https://www.drupal.org/u/akalata", + "role": "Co-maintainer" + }, + { + "name": "killes@www.drop.org", + "homepage": "https://www.drupal.org/u/killeswww.drop.org", + "role": "Co-maintainer" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/3295/committers", + "role": "contributor" + }, + { + "name": "Nafes", + "homepage": "https://www.drupal.org/user/2489926" + }, + { + "name": "ridefree", + "homepage": "https://www.drupal.org/user/49148" + } + ], + "description": "Display a sitemap.", + "homepage": "http://drupal.org/project/sitemap", + "support": { + "source": "http://cgit.drupalcode.org/sitemap", + "issues": "http://drupal.org/project/issues/sitemap" + } + }, + { + "name": "drupal/token", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/token.git", + "reference": "8.x-1.15" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/token-8.x-1.15.zip", + "reference": "8.x-1.15", + "shasum": "5916fbccc86458a5f51e71f832ac70ff4c84ebdf" + }, + "require": { + "drupal/core": "^9.2 || ^10 || ^11" + }, + "require-dev": { + "drupal/book": "*" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-1.15", + "datestamp": "1722206211", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + }, + "drush": { + "services": { + "drush.services.yml": ">=9" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "berdir", + "homepage": "https://www.drupal.org/user/214652" + }, + { + "name": "dave reid", + "homepage": "https://www.drupal.org/user/53892" + }, + { + "name": "eaton", + "homepage": "https://www.drupal.org/user/16496" + }, + { + "name": "fago", + "homepage": "https://www.drupal.org/user/16747" + }, + { + "name": "greggles", + "homepage": "https://www.drupal.org/user/36762" + }, + { + "name": "mikeryan", + "homepage": "https://www.drupal.org/user/4420" + } + ], + "description": "Provides a user interface for the Token API, some missing core tokens.", + "homepage": "https://www.drupal.org/project/token", + "support": { + "source": "https://git.drupalcode.org/project/token" + } + }, + { + "name": "drupal/token_or", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/token_or.git", + "reference": "2.3.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/token_or-2.3.0.zip", + "reference": "2.3.0", + "shasum": "d639704943132269c0a9bfec0022717deb17b083" + }, + "require": { + "drupal/core": "^9.2 || ^10 || ^11", + "drupal/token": "^1.0" + }, + "require-dev": { + "drupal/webform": "*" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "2.3.0", + "datestamp": "1730828436", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "danielbeeke", + "homepage": "https://www.drupal.org/user/2484978" + }, + { + "name": "jeroent", + "homepage": "https://www.drupal.org/user/2228934" + }, + { + "name": "pianomansam", + "homepage": "https://www.drupal.org/user/407020" + } + ], + "description": "Provides an 'or-able' token.", + "homepage": "https://www.drupal.org/project/token_or", + "support": { + "source": "https://git.drupalcode.org/project/token_or" + } + }, { "name": "drupal/tome", "version": "1.13.0", @@ -1597,6 +2636,88 @@ "source": "https://git.drupalcode.org/project/tome" } }, + { + "name": "drupal/yoast_seo", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/yoast_seo.git", + "reference": "8.x-2.1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/yoast_seo-8.x-2.1.zip", + "reference": "8.x-2.1", + "shasum": "f80745ea28937ee6e31a77ae9396cff62bb251ca" + }, + "require": { + "drupal/core": "^8 || ^9 || ^10 || ^11", + "drupal/metatag": "^1.3 || ^2.0", + "goalgorilla/rtseo.js": "^2.0" + }, + "conflict": { + "drupal/commerce": ">2.0 <2.5" + }, + "require-dev": { + "composer/installers": "^2", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1", + "dmore/behat-chrome-extension": "^1.4", + "drupal/devel": "^5.0.2", + "drupal/drupal-extension": "^5", + "drupal/paragraphs": "^1.15", + "drush/drush": "^11.0", + "friends-of-behat/mink-debug-extension": "^2.1", + "mglaman/phpstan-drupal": "^1.1.25", + "phpspec/prophecy-phpunit": "v2.0.1", + "phpstan/extension-installer": "^1.1.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1.1" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "8.x-2.1", + "datestamp": "1736565427", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "bramtenhove", + "homepage": "https://www.drupal.org/user/1549848" + }, + { + "name": "kingdutch", + "homepage": "https://www.drupal.org/user/1868952" + }, + { + "name": "Open Social", + "homepage": "https://www.drupal.org/user/272162" + }, + { + "name": "robertragas", + "homepage": "https://www.drupal.org/user/2723261" + }, + { + "name": "ronaldtebrake", + "homepage": "https://www.drupal.org/user/2314038" + } + ], + "description": "Adds Real-time SEO page analysis and configuration.", + "homepage": "http://drupal.org/project/yoast_seo", + "support": { + "source": "http://cgit.drupalcode.org/yoast_seo", + "issues": "http://drupal.org/project/yoast_seo" + } + }, { "name": "drush/drush", "version": "13.5.1", @@ -1812,6 +2933,32 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "goalgorilla/rtseo.js", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/goalgorilla/RTSEO.js.git", + "reference": "ab5f9c3ff478e0e9d61f5cc3831038e4f22bbbb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/goalgorilla/RTSEO.js/zipball/ab5f9c3ff478e0e9d61f5cc3831038e4f22bbbb0", + "reference": "ab5f9c3ff478e0e9d61f5cc3831038e4f22bbbb0", + "shasum": "" + }, + "type": "drupal-library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0" + ], + "description": "Text analysis application for use with the Real-Time SEO Drupal module.", + "homepage": "https://github.com/goalgorilla/RTSEO.js", + "support": { + "source": "https://github.com/goalgorilla/RTSEO.js/tree/2.1.0" + }, + "time": "2018-01-19T23:11:36+00:00" + }, { "name": "grasmash/expander", "version": "3.0.1", @@ -2502,6 +3649,59 @@ }, "time": "2024-07-23T14:00:32+00:00" }, + { + "name": "mtownsend/xml-to-array", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/mtownsend5512/xml-to-array.git", + "reference": "0734720a8462dba36d90fb8b2723bf46af0091f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mtownsend5512/xml-to-array/zipball/0734720a8462dba36d90fb8b2723bf46af0091f4", + "reference": "0734720a8462dba36d90fb8b2723bf46af0091f4", + "shasum": "" + }, + "require": { + "php": "~7.0|~8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Mtownsend\\XmlToArray\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Townsend", + "email": "mtownsend5512@gmail.com", + "role": "Developer" + } + ], + "description": "Easily convert valid xml to a php array.", + "keywords": [ + "array", + "convert", + "laravel", + "xml" + ], + "support": { + "issues": "https://github.com/mtownsend5512/xml-to-array/issues", + "source": "https://github.com/mtownsend5512/xml-to-array/tree/2.0.0" + }, + "time": "2021-02-27T22:39:18+00:00" + }, { "name": "nikic/php-parser", "version": "v5.4.0", @@ -6210,6 +7410,64 @@ } ], "time": "2025-01-29T07:06:14+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" } ], "packages-dev": [], diff --git a/recipes/drupal_cms_seo_tools/LICENSE.txt b/recipes/drupal_cms_seo_tools/LICENSE.txt new file mode 100644 index 000000000..94fb84639 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/recipes/drupal_cms_seo_tools/composer.json b/recipes/drupal_cms_seo_tools/composer.json new file mode 100644 index 000000000..582a26cd2 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/composer.json @@ -0,0 +1,25 @@ +{ + "name": "drupal/drupal_cms_seo_tools", + "description": "Adds common search engine optimisation tools such as meta tags, XML sitemap, robots.txt management and more.", + "type": "drupal-recipe", + "license": ["GPL-2.0-or-later"], + "require": { + "drupal/bpmn_io": "^2.0.6", + "drupal/core": ">=10.4", + "drupal/eca": "^2.1.4", + "drupal/field_group": "^3.6", + "drupal/focal_point": "^2.1", + "drupal/metatag": "^2", + "drupal/robotstxt": "^1.6", + "drupal/seo_checklist": "^5.2.1", + "drupal/simple_sitemap": "^4.2.2", + "drupal/sitemap": "^2", + "drupal/token_or": "^2.2", + "drupal/yoast_seo": "^2.1" + }, + "suggest": { + "drupal/drupal_cms_analytics": "Adds recipes for tracking website traffic.", + "drupal/drupal_cms_accessibility_tools": "Adds automated checks to help ensure content conforms to web accessibility guidelines." + }, + "version": "1.1.0" +} diff --git a/recipes/drupal_cms_seo_tools/config/eca.eca.node_sitemap_settings.yml b/recipes/drupal_cms_seo_tools/config/eca.eca.node_sitemap_settings.yml new file mode 100644 index 000000000..c65637d52 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/eca.eca.node_sitemap_settings.yml @@ -0,0 +1,200 @@ +langcode: en +status: true +dependencies: + module: + - eca_base + - eca_config + - eca_content + - eca_misc + - eca_user +id: node_sitemap_settings +modeller: bpmn_io +label: 'Automatically configure sitemap settings for content types' +version: 1.0.0 +weight: 0 +events: + Event_save_config: + plugin: 'config:save' + label: 'Create node bundle' + configuration: + config_name: node.type. + sync_mode: 'no' + write_mode: new + successors: + - + id: Activity_switch_user + condition: Flow_path_is_not_add_bundle + Event_applied_seo_tools: + plugin: 'drupal:recipe_applied' + label: 'SEO Tools applied' + configuration: + recipe_base_path: drupal_cms_seo_tools + successors: + - + id: Activity_1ghfkn0 + condition: '' +conditions: + Flow_number_of_bundles: + plugin: eca_count + configuration: + negate: false + case: false + left: bundles + right: '0' + operator: greaterthan + type: numeric + Flow_path_is_not_add_bundle: + plugin: eca_scalar + configuration: + case: false + left: '[current-page:url:path]' + right: /admin/structure/types/add + operator: equal + type: value + negate: true + Flow_config_is_empty: + plugin: eca_scalar + configuration: + negate: false + case: false + left: '[priority]' + right: priority + operator: contains + type: value + Flow_config_exists: + plugin: eca_scalar + configuration: + case: false + left: '[priority]' + right: priority + operator: contains + type: value + negate: true +gateways: + Gateway_AND_1: + type: 0 + successors: + - + id: Activity_get_bundle_id + condition: Flow_number_of_bundles +actions: + Activity_set_bundle_id_to_token: + plugin: eca_token_set_value + label: 'Remember bundle id' + configuration: + token_name: bundle + token_value: '[config:type]' + use_yaml: false + successors: + - + id: Activity_read_sitemap_config_for_bundle + condition: '' + Activity_set_index: + plugin: eca_config_write + label: 'Set index' + configuration: + config_value: '1' + use_yaml: false + save_config: true + config_name: 'simple_sitemap.bundle_settings.default.node.[bundle]' + config_key: index + successors: + - + id: Activity_set_priority + condition: '' + Activity_set_priority: + plugin: eca_config_write + label: 'Set priority' + configuration: + config_value: '0.9' + use_yaml: false + save_config: true + config_name: 'simple_sitemap.bundle_settings.default.node.[bundle]' + config_key: priority + successors: + - + id: Activity_set_changefreq + condition: '' + Activity_set_changefreq: + plugin: eca_config_write + label: 'Set changefreq' + configuration: + config_value: daily + use_yaml: false + save_config: true + config_name: 'simple_sitemap.bundle_settings.default.node.[bundle]' + config_key: changefreq + successors: + - + id: Activity_set_include_images + condition: '' + Activity_set_include_images: + plugin: eca_config_write + label: 'Set include_images' + configuration: + config_value: '0' + use_yaml: false + save_config: true + config_name: 'simple_sitemap.bundle_settings.default.node.[bundle]' + config_key: include_images + successors: + - + id: Gateway_AND_1 + condition: '' + Activity_get_list_of_bundles: + plugin: eca_get_bundle_list + label: 'Get bundles' + configuration: + token_name: bundles + type: node + mode: ids + successors: + - + id: Gateway_AND_1 + condition: '' + Activity_get_bundle_id: + plugin: eca_list_remove + label: 'Get next bundle ID' + configuration: + value: '' + token_name: bundle + method: first + index: '' + list_token: bundles + successors: + - + id: Activity_read_sitemap_config_for_bundle + condition: '' + Activity_read_sitemap_config_for_bundle: + plugin: eca_config_read + label: 'Read sitemap setting' + configuration: + token_name: priority + include_overridden: true + config_name: 'simple_sitemap.bundle_settings.default.node.[bundle]' + config_key: priority + successors: + - + id: Activity_set_index + condition: Flow_config_is_empty + - + id: Gateway_AND_1 + condition: Flow_config_exists + Activity_switch_user: + plugin: eca_switch_account + label: 'Switch user' + configuration: + user_id: '1' + successors: + - + id: Activity_set_bundle_id_to_token + condition: '' + Activity_1ghfkn0: + plugin: eca_switch_account + label: 'Switch user' + configuration: + user_id: '1' + successors: + - + id: Activity_get_list_of_bundles + condition: '' diff --git a/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_analysis.yml b/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_analysis.yml new file mode 100644 index 000000000..8cf8d78ce --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_analysis.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - node + - yoast_seo +id: node.field_seo_analysis +field_name: field_seo_analysis +entity_type: node +type: yoast_seo +settings: { } +module: yoast_seo +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_description.yml b/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_description.yml new file mode 100644 index 000000000..371aede8c --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_description.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.field_seo_description +field_name: field_seo_description +entity_type: node +type: string_long +settings: + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_image.yml b/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_image.yml new file mode 100644 index 000000000..4bac119a5 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_image.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + module: + - media + - node +id: node.field_seo_image +field_name: field_seo_image +entity_type: node +type: entity_reference +settings: + target_type: media +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_title.yml b/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_title.yml new file mode 100644 index 000000000..80becb89e --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/field.storage.node.field_seo_title.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - node +id: node.field_seo_title +field_name: field_seo_title +entity_type: node +type: string +settings: + max_length: 255 + case_sensitive: false + is_ascii: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/recipes/drupal_cms_seo_tools/config/image.style.social_media_facebook.yml b/recipes/drupal_cms_seo_tools/config/image.style.social_media_facebook.yml new file mode 100644 index 000000000..72a374063 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/image.style.social_media_facebook.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + module: + - focal_point +name: social_media_facebook +label: 'Social media - Facebook' +effects: + 338d92b4-4580-4118-aa11-96fb253e343e: + uuid: 338d92b4-4580-4118-aa11-96fb253e343e + id: focal_point_scale_and_crop + weight: 1 + data: + width: 1200 + height: 630 + crop_type: focal_point + 8ddd479d-03db-45d3-8591-a7b72e13bb21: + uuid: 8ddd479d-03db-45d3-8591-a7b72e13bb21 + id: image_convert + weight: 2 + data: + extension: webp diff --git a/recipes/drupal_cms_seo_tools/config/image.style.social_media_x.yml b/recipes/drupal_cms_seo_tools/config/image.style.social_media_x.yml new file mode 100644 index 000000000..f972980df --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/image.style.social_media_x.yml @@ -0,0 +1,22 @@ +langcode: en +status: true +dependencies: + module: + - focal_point +name: social_media_x +label: 'Social media - X (Twitter)' +effects: + 7de498f6-16e8-4f33-aa2b-afc30ad91155: + uuid: 7de498f6-16e8-4f33-aa2b-afc30ad91155 + id: focal_point_scale_and_crop + weight: 1 + data: + width: 1600 + height: 900 + crop_type: focal_point + 94553ea6-7659-4f21-b586-3dd07fb21e88: + uuid: 94553ea6-7659-4f21-b586-3dd07fb21e88 + id: image_convert + weight: 2 + data: + extension: webp diff --git a/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.front.yml b/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.front.yml new file mode 100644 index 000000000..3bbddf2f0 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.front.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +id: front +label: 'Front page' +tags: + canonical_url: '[site:url]' + description: '[node:field_seo_description|node:field_description]' + image_src: '[node:field_seo_image:entity:field_media_image:social_media_facebook|node:field_image:entity:field_media_image:social_media_facebook]' + og_description: '[node:field_seo_description|node:field_description]' + og_image: '[node:field_seo_image:entity:field_media_image:social_media_facebook|node:field_image:entity:field_media_image:social_media_facebook]' + og_image_alt: '[node:field_seo_image:entity:field_media_image:alt|node:field_image:entity:field_media_image:alt]' + og_image_height: '[node:field_seo_image:entity:field_media_image:social_media_facebook:height|node:field_image:entity:field_media_image:social_media_facebook:height]' + og_image_type: '[node:field_seo_image:entity:field_media_image:social_media_facebook:mimetype|node:field_image:entity:field_media_image:social_media_facebook:mimetype]' + og_image_width: '[node:field_seo_image:entity:field_media_image:social_media_facebook:width|node:field_image:entity:field_media_image:social_media_facebook:width]' + og_site_name: '[site:name]' + og_title: '[node:field_seo_title|node:title]' + og_type: website + og_updated_time: '[node:changed:custom:c]' + og_url: '[current-page:url:absolute]' + referrer: unsafe-url + rights: 'Copyright ©[date:html_year] All rights reserved.' + shortlink: '[site:url]' + title: '[node:field_seo_title|node:title] | [site:name]' + twitter_cards_description: '[node:field_seo_description|node:field_description]' + twitter_cards_image: '[node:field_seo_image:entity:field_media_image:social_media_x:url|node:field_image:entity:field_media_image:social_media_x:url]' + twitter_cards_image_alt: '[node:field_seo_image:entity:field_media_image:alt|node:field_image:entity:field_media_image:alt]' + twitter_cards_title: '[node:field_seo_title|node:title]' + twitter_cards_type: summary_large_image diff --git a/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.global.yml b/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.global.yml new file mode 100644 index 000000000..4ba5204f5 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.global.yml @@ -0,0 +1,13 @@ +langcode: en +status: true +id: global +label: Global +tags: + referrer: unsafe-url + title: '[current-page:title] | [site:name]' + canonical_url: '[current-page:url:absolute]' + shortlink: '[current-page:url:unaliased]' + rights: 'Copyright ©[date:html_year] All rights reserved.' + og_url: '[current-page:url:absolute]' + og_site_name: '[site:name]' + twitter_cards_type: summary diff --git a/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.node.yml b/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.node.yml new file mode 100644 index 000000000..b4b592356 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/config/metatag.metatag_defaults.node.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +id: node +label: Content +tags: + canonical_url: '[current-page:url:absolute]' + description: '[node:field_seo_description|node:field_description]' + image_src: '[node:field_seo_image:entity:field_media_image:social_media_facebook|node:field_featured_image:entity:field_media_image:social_media_facebook]' + og_description: '[node:field_seo_description|node:field_description]' + og_image: '[node:field_seo_image:entity:field_media_image:social_media_facebook|node:field_featured_image:entity:field_media_image:social_media_facebook]' + og_image_alt: '[node:field_seo_image:entity:field_media_image:alt|node:field_featured_image:entity:field_media_image:alt]' + og_image_height: '[node:field_seo_image:entity:field_media_image:social_media_facebook:height|node:field_featured_image:entity:field_media_image:social_media_facebook:height]' + og_image_type: '[node:field_seo_image:entity:field_media_image:social_media_facebook:mimetype|node:field_featured_image:entity:field_media_image:social_media_facebook:mimetype]' + og_image_width: '[node:field_seo_image:entity:field_media_image:social_media_facebook:width|node:field_featured_image:entity:field_media_image:social_media_facebook:width]' + og_site_name: '[site:name]' + og_title: '[node:field_seo_title|node:title]' + og_type: article + og_updated_time: '[node:changed:custom:c]' + og_url: '[current-page:url:absolute]' + referrer: unsafe-url + rights: 'Copyright ©[date:html_year] All rights reserved.' + shortlink: '[site:url]' + title: '[node:field_seo_title|node:title] | [site:name]' + twitter_cards_description: '[node:field_seo_description|node:field_description]' + twitter_cards_image: '[node:field_seo_image:entity:field_media_image:social_media_x:url|node:field_featured_image:entity:field_media_image:social_media_x:url]' + twitter_cards_image_alt: '[node:field_seo_image:entity:field_media_image:alt|node:field_featured_image:entity:field_media_image:alt]' + twitter_cards_title: '[node:field_seo_title|node:title]' + twitter_cards_type: summary_large_image diff --git a/recipes/drupal_cms_seo_tools/recipe.yml b/recipes/drupal_cms_seo_tools/recipe.yml new file mode 100644 index 000000000..17021f2a1 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/recipe.yml @@ -0,0 +1,236 @@ +name: SEO Tools +description: Adds common search engine optimisation tools such as meta tags, XML sitemap, robots.txt management and more. +type: Drupal CMS +recipes: + - core/recipes/image_media_type +install: + - eca_base + - eca_config + - eca_content + - eca_misc + - eca_user + - field_group + - focal_point + - layout_builder + - menu_link_content + - metatag + - metatag_open_graph + - metatag_twitter_cards + - node + - robotstxt + - seo_checklist + - simple_sitemap + - sitemap + - token_or + - yoast_seo +config: + # Treat all field storages strictly, since they influence the database layout. + strict: + - field.storage.node.field_seo_analysis + - field.storage.node.field_seo_description + - field.storage.node.field_seo_image + - field.storage.node.field_seo_title + import: + metatag: + # Exclude Global, Front and Node as we create those in the config folder. + - metatag.metatag_defaults.403.yml + - metatag.metatag_defaults.404.yml + - metatag.metatag_defaults.taxonomy_term.yml + - metatag.metatag_defaults.user.yml + simple_sitemap: '*' + actions: + # Add SEO fields to all content types that are installed. + field.storage.node.field_seo_analysis: + addToAllBundles: + label: 'SEO analysis' + description: 'Pick the main keyword this page is about.' + field.storage.node.field_seo_description: + addToAllBundles: + label: 'SEO description' + description: 'Write a description for search engines and social media sites.' + field.storage.node.field_seo_image: + addToAllBundles: + label: 'SEO image' + description: 'Upload an image to be used when the page is shared on social media sites.' + # Ensure all instances of `field_seo_image` reference image media. + field.field.node.*.field_seo_image: + setSettings: + handler: 'default:media' + handler_settings: + target_bundles: + image: image + field.storage.node.field_seo_title: + addToAllBundles: + label: 'SEO title' + description: 'Use this field to overwrite the default HTML page title for SEO.' + # Configure the fields on the edit forms. + core.entity_form_display.node.*.default: + # Configure field group. + setThirdPartySettings: + - module: field_group + key: group_seo + value: + children: + - field_seo_title + - field_seo_description + - field_seo_image + - field_seo_analysis + label: 'Search Engine Optimization (SEO) Information' + region: content + parent_name: '' + weight: 50 + format_type: details + format_settings: + classes: '' + show_empty_fields: false + id: group_seo + label_as_html: false + open: false + description: '' + required_fields: false + setComponents: + - + name: field_seo_analysis + options: + type: yoast_seo_widget + weight: 18 + region: content + settings: + render_theme: NULL + render_view_mode: default + edit_title: 0 + edit_description: 0 + - + name: field_seo_description + options: + type: string_textarea + weight: 16 + region: content + settings: + rows: 5 + placeholder: '' + - + name: field_seo_image + options: + type: media_library_widget + weight: 17 + region: content + settings: + media_types: { } + - + name: field_seo_title + options: + type: string_textfield + weight: 15 + region: content + settings: + size: 60 + placeholder: '' + robotstxt.settings: + # We add Drupal core's robots.txt and XML Sitemap. + simpleConfigUpdate: + content: | + # + # robots.txt + # + # This file is to prevent the crawling and indexing of certain parts + # of your site by web crawlers and spiders run by sites like Yahoo! + # and Google. By telling these "robots" where not to go on your site, + # you save bandwidth and server resources. + # + # This file will be ignored unless it is at the root of your host: + # Used: http://example.com/robots.txt + # Ignored: http://example.com/site/robots.txt + # + # For more information about the robots.txt standard, see: + # http://www.robotstxt.org/robots + User-agent: * + # CSS, JS, Images + Allow: /core/*.css$ + Allow: /core/*.css? + Allow: /core/*.js$ + Allow: /core/*.js? + Allow: /core/*.gif + Allow: /core/*.jpg + Allow: /core/*.jpeg + Allow: /core/*.png + Allow: /core/*.svg + Allow: /profiles/*.css$ + Allow: /profiles/*.css? + Allow: /profiles/*.js$ + Allow: /profiles/*.js? + Allow: /profiles/*.gif + Allow: /profiles/*.jpg + Allow: /profiles/*.jpeg + Allow: /profiles/*.png + Allow: /profiles/*.svg + # Directories + Disallow: /core/ + Disallow: /profiles/ + # Files + Disallow: /README.md + Disallow: /composer/Metapackage/README.txt + Disallow: /composer/Plugin/ProjectMessage/README.md + Disallow: /composer/Plugin/Scaffold/README.md + Disallow: /composer/Plugin/VendorHardening/README.txt + Disallow: /composer/Template/README.txt + Disallow: /modules/README.txt + Disallow: /sites/README.txt + Disallow: /themes/README.txt + # Paths (clean URLs) + Disallow: /admin/ + Disallow: /comment/reply/ + Disallow: /filter/tips + Disallow: /node/add/ + Disallow: /search/ + Disallow: /user/register + Disallow: /user/password + Disallow: /user/login + Disallow: /user/logout + Disallow: /media/oembed + Disallow: /*/media/oembed + # Paths (no clean URLs) + Disallow: /index.php/admin/ + Disallow: /index.php/comment/reply/ + Disallow: /index.php/filter/tips + Disallow: /index.php/node/add/ + Disallow: /index.php/search/ + Disallow: /index.php/user/password + Disallow: /index.php/user/register + Disallow: /index.php/user/login + Disallow: /index.php/user/logout + Disallow: /index.php/media/oembed + Disallow: /index.php/*/media/oembed + # XML sitemap + Sitemap: /sitemap.xml + sitemap.settings: + simpleConfigUpdate: + plugins.frontpage: + base_plugin: frontpage + enabled: true + weight: 0 + settings: + title: 'Front page' + rss: null + id: frontpage + provider: sitemap + plugins.menu:main: + base_plugin: menu + enabled: true + weight: 0 + settings: + title: '' + show_disabled: false + id: 'menu:main' + provider: sitemap + system.performance: + simpleConfigUpdate: + # Ensure caching is on and set to 15 minutes for Lighthouse. + cache.page.max_age: 900 + # Ensure CSS and JS are preprocessed. + css.preprocess: true + js.preprocess: true + user.role.anonymous: + grantPermission: 'access sitemap' + user.role.authenticated: + grantPermission: 'access sitemap' diff --git a/recipes/drupal_cms_seo_tools/tests/src/Functional/ComponentValidationTest.php b/recipes/drupal_cms_seo_tools/tests/src/Functional/ComponentValidationTest.php new file mode 100644 index 000000000..c38e2a9cf --- /dev/null +++ b/recipes/drupal_cms_seo_tools/tests/src/Functional/ComponentValidationTest.php @@ -0,0 +1,139 @@ +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.'); + } + +} diff --git a/recipes/drupal_cms_seo_tools/tests/src/Functional/ContentMetaTagsTest.php b/recipes/drupal_cms_seo_tools/tests/src/Functional/ContentMetaTagsTest.php new file mode 100644 index 000000000..961b20e44 --- /dev/null +++ b/recipes/drupal_cms_seo_tools/tests/src/Functional/ContentMetaTagsTest.php @@ -0,0 +1,160 @@ +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('')->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); + } + +}