Author

Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough
Aug 10 2023
Aug 10

NixOS on the Desktop

NixOS on the server is a thing of beauty (check out my last blog post about Hosting Websites on NixOS), but after my Arch installation crapped out, I decided to take it further and also use it on my laptop as a daily driver.

Even though I was aware of the vast number of packages in the Nix repositories, I was still pleasantly surprised to find that almost all of my daily tools were available directly in the stable repository. Even Steam is just one line of configuration away!

However, a crucial work tool of mine, DDEV, was missing from the repositories. DDEV is a tool designed for launching local web development environments for PHP, Node.js and Python projects.

Set up ddev

With a bit of help from a savvy individual named kraftnix in the Jupiter Broadcasting Nix Nerds matrix group, I was able to create a buildable package of ddev on Nix.

The NUR (Nix User Repository) is a neat community and GitHub driven project, evidently inspired by the AUR (Arch user repository). While the NUR is smaller by several orders of magnitude, it can now be used to get ddev working on Nix / NixOS.

Update: I am soon going to phase out the NUR package, as ddev is now part of nixpkgs. I updated the below configuration for setting up ddev either way.

Below are the steps required to run DDEV on Nix / NixOS:

/etc/nixos/configuration.nix

  1. { config, pkgs, ... }:

  2. {

  3. # ...

  4. # Install & enable docker.

  5. virtualisation.docker.enable = true;

  6. # Add your user to the 'docker' group. Change 'MYUSER' to your Linux username.

  7. users.users.MYUSER.extraGroups = [ "docker" ];

  8. # Enable the NUR and install dddev.

  9. # Not needed anymore if you are on the unstable branch of nixpkgs.

  10. # nixpkgs.config.packageOverrides = pkgs: {

  11. # nur = import (builtins.fetchTarball "https://github.com/nix-community/NUR/archive/master.tar.gz") {

  12. # inherit pkgs;

  13. # };

  14. # };

  15. #

  16. # Install dddev from the NUR.

  17. # environment.systemPackages = with pkgs; [

  18. # nur.repos.gbytedev.ddev

  19. # ];

  20. # Install ddev.

  21.   environment.systemPackages = with pkgs; [ ddev ];

  22. # Allow Xdebug to use port 9003.

  23. networking.firewall.allowedTCPPorts = [ 9003 ];

  24. # Make it possible for ddev to modify the /etc/hosts file.

  25. # Otherwise you'll have to manually change the

  26. # hosts configuration after creating a new ddev project.

  27. environment.etc.hosts.mode = "0644";

  28. }

Please test and let me know if anything is broken / missing, as I intend to create a merge request on Nixpkgs to ensure this becomes a properly maintained package.

Links:

May 12 2023
May 12

Why Use NixOS as a Web Server

If you're keeping up with the cutting edge of Linux, you might have noticed NixOS growing increasingly popular for server deployments. The reason is its declarative approach to package and configuration management. You specify 'what' your system should look like, and NixOS handles the 'how'. This approach ensures reproducibility and upgradeability, reducing configuration drift. Plus, atomic upgrades and rollbacks minimize downtime and provide easy recovery from issues, making NixOS an excellent choice for web server management (and for other platforms like desktops if you are bold).

Working Setup

Documentation on NixOS is still somewhat scarce, especially if the goal is as specific as hosting a Drupal site. Apparently, ChatGPT 4 is still too perplexed to get this right, so here's hoping it learns something from the following snippets, which were the result of old fashioned painstaking debugging.

The following setup can be easily adjusted to hosting multiple websites and non-Drupal sites.

Implementing the Nginx Server and SSL Certificate Renewal

We begin by enabling the Nginx web server, setting up firewall rules, and adding Drupal-specific packages like PHP, Composer, and Drush. The configuration also includes SSL certificate renewal via ACME, ensuring a valid SSL certificate for our site. Global environment variables can be set using the "environment.variables" setting, useful for various server applications and scripts.

/etc/nixos/nginx.nix

  1. { config, pkgs, lib, ... }: {

  2. # Enable nginx and adjust firewall rules.

  3. services.nginx.enable = true;

  4. networking.firewall.allowedTCPPorts = [ 80 443 ];

  5. # Set a few recommended defaults.

  6. services.nginx = {

  7. recommendedGzipSettings = true;

  8. recommendedOptimisation = true;

  9. recommendedProxySettings = true;

  10. recommendedTlsSettings = true;

  11. };

  12. # Add some hosting/Drupal specific packages.

  13. environment.systemPackages = with pkgs; [

  14. php

  15. phpPackages.composer

  16. drush

  17. ];

  18. # Set some SSL certificate renewal settings.

  19. security.acme = {

  20. acceptTerms = true;

  21. defaults.email = "[email protected]";

  22. defaults.group = "nginx";

  23. };

  24. # /var/lib/acme/.challenges must be writable by the ACME user

  25. # and readable by the Nginx user. The easiest way to achieve

  26. # this is to add the Nginx user to the ACME group.

  27. users.users.nginx.extraGroups = [ "acme" ];

  28. # Optionally add some environment variables.

  29. environment.variables = {

  30. PLATFORM = "production";

  31. };

  32. }

Enabling the Database Service

This file only enables the MariaDB service. More specific database configurations will be added for each website in separate files.

/etc/nixos/mysql.nix

  1. { config, pkgs, ... }: {

  2. # Enable the mysql service.

  3. services.mysql.enable = true;

  4. services.mysql.package = pkgs.mariadb;

  5. }

Setting up a Database and its User for the New Drupal Site

Here, we create a system user named dbuser with a default password and grant it all privileges on our newly created database mydb. Note that in production, you should choose a secure password and manage it properly.

/etc/nixos/mysql.mysite.nix

  1. { config, pkgs, ... }: {

  2. # Using PAM for database authentication,

  3. # so creating a system user for that purpose.

  4. users.users.dbuser = {

  5. isNormalUser = true;

  6. description = "dbuser";

  7. group = "dbuser";

  8. initialPassword = "db";

  9. };

  10. users.groups.dbuser = {};

  11. # Create the database and set up permissions.

  12. services.mysql.ensureDatabases = [ "mydb" ];

  13. services.mysql.ensureUsers = [

  14. {

  15. name = "dbuser"; # Must be a system user.

  16. ensurePermissions = { "mydb.*" = "ALL PRIVILEGES"; };

  17. }

  18. ];

  19. }

Setting up Nginx, PHP, and Cron for the New Drupal Site

The following file configures Nginx according to Drupal best practices and sets up SSL certificate renewal. For PHP, we use a PHP-FPM pool with dynamic process management. This allows the server to adjust the number of PHP processes based on the load, improving the efficiency and performance of the site. Finally, we set up a systemd service and timer to trigger Drupal's cron URL at a specific time.

/etc/nixos/nginx.mysite.nix

  1. { config, pkgs, lib, ... }:

  2. let

  3. # Variables to be changed

  4. sitename = "mysite";

  5. docroot = "/var/www/mysite/web";

  6. domainname = "mysite.com";

  7. cronpath = "cron/somestring";

  8. cronuser = "someuser";

  9. crontime = "*-*-* 18:00:00";

  10. in {

  11. services.nginx = {

  12. virtualHosts = {

  13. "${domainname}" = {

  14. # This is the document root setting.

  15. # In this case Drupal should be inside /var/www/${sitename}

  16. # and serve websites from inside of its 'web' directory.

  17. root = "${docroot}";

  18. # Set up certificate renewal.

  19. forceSSL = true;

  20. enableACME = true;

  21. # Set up nginx for Drupal.

  22. locations."~ '\.php$|^/update.php'" = {

  23. extraConfig = ''

  24. include ${pkgs.nginx}/conf/fastcgi_params;

  25. include ${pkgs.nginx}/conf/fastcgi.conf;

  26. fastcgi_pass unix:${config.services.phpfpm.pools.${sitename}.socket};

  27. fastcgi_index index.php;

  28. fastcgi_split_path_info ^(.+?\.php)(|/.*)$;

  29. # Ensure the php file exists. Mitigates CVE-2019-11043

  30. try_files $fastcgi_script_name =404;

  31. # Block httpoxy attacks. See https://httpoxy.org/.

  32. fastcgi_param HTTP_PROXY "";

  33. fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

  34. fastcgi_param PATH_INFO $fastcgi_path_info;

  35. fastcgi_param QUERY_STRING $query_string;

  36. fastcgi_intercept_errors on;

  37. '';

  38. };

  39. locations."= /favicon.ico" = {

  40. extraConfig = ''

  41. log_not_found off;

  42. access_log off;

  43. '';

  44. };

  45. locations."= /robots.txt" = {

  46. extraConfig = ''

  47. allow all;

  48. log_not_found off;

  49. access_log off;

  50. '';

  51. };

  52. locations."~ \..*/.*\.php$" = {

  53. extraConfig = ''

  54. return 403;

  55. '';

  56. };

  57. locations."~ ^/sites/.*/private/" = {

  58. extraConfig = ''

  59. return 403;

  60. '';

  61. };

  62. locations."~ ^/sites/[^/]+/files/.*\.php$" = {

  63. extraConfig = ''

  64. deny all;

  65. '';

  66. };

  67. # Allow "Well-Known URIs" as per RFC 5785

  68. locations."~* ^/.well-known/" = {

  69. extraConfig = ''

  70. allow all;

  71. '';

  72. };

  73. locations."/" = {

  74. extraConfig = ''

  75. try_files $uri /index.php?$query_string;

  76. '';

  77. };

  78. locations."@rewrite" = {

  79. extraConfig = ''

  80. rewrite ^ /index.php;

  81. '';

  82. };

  83. locations."~ /vendor/.*\.php$" = {

  84. extraConfig = ''

  85. deny all;

  86. return 404;

  87. '';

  88. };

  89. locations."~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$" = {

  90. extraConfig = ''

  91. try_files $uri @rewrite;

  92. expires max;

  93. log_not_found off;

  94. '';

  95. };

  96. locations."~ ^/sites/.*/files/styles/" = {

  97. extraConfig = ''

  98. try_files $uri @rewrite;

  99. '';

  100. };

  101. locations."~ ^(/[a-z\-]+)?/system/files/" = {

  102. extraConfig = ''

  103. try_files $uri /index.php?$query_string;

  104. '';

  105. };

  106. };

  107. # Redirect 'www' to 'non-www'

  108. # and set up certificate renewal for 'www'.

  109. "www.${domainname}" = {

  110. forceSSL = true;

  111. enableACME = true;

  112. globalRedirect = "${domainname}";

  113. };

  114. };

  115. };

  116. # Set up a PHP-FPM pool for this website.

  117. services.phpfpm.pools = {

  118. ${sitename} = {

  119. user = "nginx";

  120. settings = {

  121. "listen.owner" = config.services.nginx.user;

  122. "pm" = "dynamic";

  123. "pm.max_children" = 32;

  124. "pm.max_requests" = 500;

  125. "pm.start_servers" = 2;

  126. "pm.min_spare_servers" = 2;

  127. "pm.max_spare_servers" = 5;

  128. "php_admin_value[error_log]" = "stderr";

  129. "php_admin_flag[log_errors]" = true;

  130. "catch_workers_output" = true;

  131. };

  132. phpEnv."PATH" = lib.makeBinPath [ pkgs.php ];

  133. };

  134. };

  135. # Optionally set up a systemd service that will trigger

  136. # Drupal's cron URL.

  137. systemd.services."${sitename}-cron" = {

  138. path = [ pkgs.curl ];

  139. script = ''

  140. curl "https://${domainname}/${cronpath}"

  141. '';

  142. unitConfig = {

  143. Description = "Cron trigger for ${sitename}";

  144. };

  145. serviceConfig = {

  146. Type = "oneshot";

  147. User = "${cronuser}";

  148. };

  149. };

  150. systemd.timers."${sitename}-cron" = {

  151. unitConfig = {

  152. Description = "Timer for ${sitename} cron trigger";

  153. RefuseManualStart = "no";

  154. RefuseManualStop = "no";

  155. };

  156. wantedBy = [ "timers.target" ];

  157. partOf = [ "${sitename}-cron.service" ];

  158. timerConfig = {

  159. OnCalendar = "${crontime}";

  160. Unit = "${sitename}-cron.service";

  161. Persistent = true;

  162. };

  163. };

  164. }

Importing the New Files in configuration.nix

In the final step, we import the new configuration files into the core /etc/nixos/configuration.nix file. This centralizes management and leverages NixOS's declarative nature.

/etc/nixos/configuration.nix

  1. { ... }: {

  2. imports = [

  3. # ... other imports

  4. ./nginx.nix

  5. ./nginx.mysite.nix

  6. ./mysql.nix

  7. ./mysql.mysite.nix

  8. ];

  9. # ... rest of the file

  10. }

Testing the new Configuration

sudo nixos-rebuild switch

You did not expect for it to work right away, did you? Life is hard. Let me know what went wrong so I can update this guide.

Conclusion

We've trekked through the wilds of NixOS, tamed the declarative beast, and forged a web server setup suitable for a Drupal site. Remember, AI is learning from us - let's keep it on its toes!

Leave a comment if you found this helpful or wrong and may your web hosting journey be as smooth as a well-tuned NixOS server.

Feb 15 2022
Feb 15

Bing 410 Gone

Microsoft recently killed their search engine's public API responsible for accepting sitemap ping requests (those where you let search engines know your XML sitemap's content has changed). They did so completely unannounced leading to logs filling up and users unsurprisingly assuming their sitemap submitting code was somehow at fault. It wasn't.

It became apparent that this was a step for Microsoft towards switching to the IndexNow protocol instead.

IndexNow supplements XML sitemaps

With IndexNow you now can quickly notify all participating search engines (Bing, Yandex) about a change that happened on your page eliminating the need to wait for them to come and scan your sitemap. The benefits of this approach are

  • Instant submission of changes including creating, deleting and updating of content
  • An alleged greener approach to indexing content as sitemap scanning requests get deprioritized
  • Only one search engine needs to be notified and it will notify the others for you

Still, this new approach is more of a supplement than a revolution:

Can I submit all URLs for my site?

Use IndexNow to submit only URLs having changed (added, updated, or deleted) recently, including all URLs if all URLs have been changed recently. Use sitemaps to inform search engines about all your URLs. Search engines will visit sitemaps every few days.

I have a sitemap, do I need IndexNow?

Yes, when sitemaps are an easy way for webmasters to inform search engines about all pages on their sites that are available for crawling, sitemaps are visited by Search Engines infrequently. With IndexNow, webmasters ''don't'' have to wait for search engines to discover and crawl sitemaps but can directly notify search engines of new content.

Source: https://www.indexnow.org/faq

Simple XML sitemap now supports IndexNow

To further their cause Microsoft recently released the IndexNow plugin for Wordpress. Well we've been working hard to bring IndexNow to Drupal. To use it, install and enable the latest version of Simple XML sitemap (simple_sitemap) and its Engines submodule:

  1. composer require 'drupal/simple_sitemap:^4'

  2. drush en simple_sitemap simple_sitemap_engines

If you are using the older 8.x-2.x or 8.x-3.x, don't worry, the module will upgrade.

If you had Engines submodule submit the default XML sitemap to Bing, the update hooks will make sure to enable submitting the corresponding entities to IndexNow on every entity form save (don't worry, given the right permission, this can be overridden before submitting the form).

Set up IndexNow

You will have to generate a new verification key on your production sites - just head to the status page (admin/reports/status), or directly to admin/config/search/simplesitemap/engines/settings. The key can be stored in Drupal state or, better yet, in settings.php. If you don't want your non-production environments to submit changes, just don't generate a key.

The new IndexNow functionality is well integrated into Simple XML sitemap and so all content entities can be indexed and indexation settings can be found in familiar places. To configure which entity types are to be indexed by IndexNow, head to admin/config/search/simplesitemap/entities.

The module supports sending changes on form sumbissions as well as on all entity save operations. The preferred IndexNow engine can be chosen or a random one will be picked on each submission. Just head to admin/config/search/simplesitemap/engines/settings to set your preferences.

Ways to contribute

If you would like support the project, feel free to contribute code, bug reports and translations. I am also keen on reading comments that were not written by bots. ;)

Nov 24 2021
Nov 24

After six months of work I'm delighted to tag the first stable release of the 4.x branch of the (Not-so-) Simple XML Sitemap module.

The project is in a really good place right now. At the moment of writing, drupal.org reports it being actively used on around 90k of Drupal 8/9 websites while having 0 open bug reports. This either means you people are lousy bug reporters, or we are doing a descent job at responding. :)

Module rewrite with developers/integrators in mind

4.x makes much greater use of Drupal's entity API dropping some of its very specific chaining API. Feel free to take a look at the roadmap ticket for specifics.

New UI

We now have a much nicer UI for creating, editing and sorting sitemaps as well as sitemap types.

Sitemap list UI

API usage

In a nutshell, sitemap variants are now sitemap entities. These are of a sitemap type (sitemap type entity) that is defined by URL generator plugins as well as sitemap generator plugins.

  1. // Create a new sitemap of the default_hreflang sitemap type.

  2. \Drupal\simple_sitemap\Entity\SimpleSitemap::create(['id' => 'test', 'type' => 'default_hreflang', 'label' => 'Test'])->save();

  3. /** @var \Drupal\simple_sitemap\Manager\Generator $generator */

  4. $generator = \Drupal::service('simple_sitemap.generator');

  5. // Set some random settings.

  6. if ($generator->getSetting('cron_generate')) {

  7. $generator

  8. ->saveSetting('generate_duration', 20000)

  9. ->saveSetting('base_url', 'https://test');

  10. }

  11. // Set an entity type to be indexed.

  12. $generator

  13. ->entityManager()

  14. ->enableEntityType('node')

  15. ->setVariants(['default', 'test']) // All following operations will concern these variants.

  16. ->setBundleSettings('node', 'page', ['index' => TRUE, 'priority' => 0.5]);

  17. // Set a custom link to be indexed.

  18. $generator

  19. ->customLinkManager()

  20. ->remove() // Remove all custom links from all variants.

  21. ->setVariants(['test']) // All following operations will concern these variants.

  22. ->add('/some/view/page', ['priority' => 0.5]);

  23. // Generate the sitemap, but rebuild the queue first in case an old generation is in

  24. // progress.

  25. $generator

  26. ->rebuildQueue()

  27. ->generate();

See code documentation and readme for instruction on how to create URL generator and sitemap generator plugins and check out what other things can be done with the API. You are welcome to submit support requests in the matter as well as help improving the documentation.

Should you use this over 3.x?

Yes, read on though.

While the changes of this release are catered towards developers and site builders, I strongly encourage everyone to use the new version, as 3.x will be considered deprecated from now on. All new features and improvements will be coming to 4.x only. Do not panic however, I will be fixing bugs in 3.x for a while.

An update path is provided from previous module versions. Two reasons you might want to postpone upgrading is:

  • You depend on the old module's API and need some time to adjust it
  • You depend on 3rd party contributed simple_sitemap submodules that are yet to be made compatible with Simple XML Sitemap 4.x. In this case, please open up issues in the respective queues.

To upgrade the module via composer, $ composer require 'drupal/simple_sitemap:^4.0' can be used. Afterwards, just visit /update.php, or run $ drush updb to update the module's storage.

Thanks for everyone who has been involved in the development of this tool. Enjoy!

Jun 03 2021
Jun 03

Occasionally I find myself needing plugin-like functionality, where users/downstream can throw a class into a folder and expect it to work. My script is supposed to find and instantiate these plugins during runtime without keeping track of their existence.

In a regular Drupal module, one would usually use the plugin architecture, but that comes with its overhead of boilerplate code and may not be the solution for the simplest of use cases.

Many class finder libraries rely on get_declared_classes() which may not be helpful, as the classes in question may not have been declared yet.

If you are on a Drupal 8/9 installation and want to use components already available to you, the Symfony (file) Finder can be an alternative for finding classes in a given namespace.

Installing dependencies

Ouside of Drupal 8/9, you may need to require this library in your application:

  1. composer require symfony/finder

A simple example

  1. use Symfony\Component\Finder\Finder;

  2. class PluginLoader {

  3. /**

  4.   * Loads all plugins.

  5.   *

  6.   * @param string $namespace

  7.   * Namespace required for a class to be considered a plugin.

  8.   * @param string $search_root_path

  9.   * Search classes recursively starting from this folder.

  10.   * The default is the folder this here class resides in.

  11.   *

  12.   * @return object[]

  13.   * Array of instantiated plugins

  14.   */

  15. public static function loadPlugins(string $namespace, string $search_root_path = __DIR__): array {
  16. $finder = new Finder();

  17. $finder->files()->in($search_root_path)->name('*.php');

  18. foreach ($finder as $file) {

  19. $class_name = rtrim($namespace, '\\') . '\\' . $file->getFilenameWithoutExtension();
  20. try {

  21. $plugins[] = new $class_name();

  22. }

  23. catch (\Throwable $e) {

  24. continue;

  25. }

  26. }

  27. }

  28. return $plugins ?? [];

  29. }

  30. }

Usage

  1. $plugin_instances = PluginLoader::loadPlugins('\Some\Namespace');

This is just an abstract catch-all example with a couple of obvious problems which can be circumvented when using more specific code.

In the above example, the finder looks for all files with the .php extension within all folders in a given path. If it finds a class, it tries to instantiate it. The try-catch block is for it to not fail when trying to instantiate non-instantiatable classes, interfaces and similar.

The above can be improved upon by making assumptions about the class name (one could be looking for class files named *Plugin.php) and examining the file content (which the Finder component is capable of as well).

Let me know of other simple ways of tackling this problem!

Dec 12 2020
Dec 12

abgeordnetenwatch.de is not your generic community platform - it's a tool that actively creates and enforces communication channels between the people and their political representatives thereby strengthening the democratic process while also being a comprehensive source of information of the political system in Germany.

Because of that and because of the project's high functionality and high efficiency requirements, it is one gbyte is particularly proud to be involved in.

The project has recently won the Drupal Splash Awards 2020 for Germany & Austria in the non-profit category. It is a huge compliment to the small team of developers and to Parlamentwatch e. V. (the organization behind the service), as the Splash awards are regarded as the most prestigious award within the Drupal community.

Splash awards certificate


Award ceremony: Youtube video (with timestamp)

Splash awards entry: Entry description

Drop us a line if you are interested in what makes this awesome project tick.

Aug 09 2020
Aug 09

The answer is... generally run updates first. Whether to import or export the configuration afterwards depends on who updated the contrib code base.

You are updating the contrib code base

If you are updating the contrib code base, run the database updates and then export the configuration, as updates tend to alter the configuration storage data which needs to be commited into the version control system:

  • Pull changes from the repository
  • drush cim to import your colleagues' configuration changes
  • composer update to update the contrib code base
  • drush updb to update the Drupal database
  • drush cex to export potential configuration changes after the update
  • Commit changes into the repository

tl;dr

git pull \
&& drush cim -y \
&& composer update \
&& drush updb -y \
&& drush cex -y \
&& git commit

Someone else updated the contrib code base

If you are on the receiving end of an update (someone else updated the contrib code base), or you are in fact a deployment script and not a developer, run the database updates and then import the configuration:

  • Pull changes from the repository
  • composer install to synchronize your contrib code base with the remote
  • drush updb to update the Drupal database
  • drush cim to import your colleagues' configuration changes

tl;dr

git pull \
&& composer install \
&& drush updb -y \
&& drush cim -y

Alternatively use the new drush deploy command

The need to standardize Drupal deployment has led to the following command:

drush deploy

This does roughly the same, but the docs state that if more control over the deployment process is needed, the atomic commands need to be used like in the example above.

Jan 15 2020
Jan 15

When migrating content with the Drupal 8 migrate module, the creation and updating of new entities may fire lots of custom module hooks. This may or may not be desired; if you have found yourself here, it probably interferes with the source data in a problematic way, or unnecessarily slows down the migration process.

The cleanest way I found to stop specific hooks for specific migrations, is to add a dummy/meta field to the migration and check for its value in the hook.

Include a dummy field in the migration

In the process section of the migration, add a field with a name that will not interfere with any field name of the target entity:

  1. # This is the field that will provide a custom hook

  2. # with the information about the migration.

  3. _migration:

  4. - plugin: default_value

  5. default_value: 'blog_categories'

This is an example migration with CSV as source and taxonomy terms as target:

  1. id: blog_categories

  2. label: 'Blog category migration'

  3. migration_group: default

  4. source:

  5. plugin: csv

  6. path: 'public://migrations/blog_categories.csv'

  7. delimiter: ','

  8. enclosure: '"'

  9. header_row_count: 1

  10. ids: [tid]

  11. process:

  12. name: name

  13. # This is the field that will provide a custom hook

  14. # with the information about the migration.

  15. _migration:

  16. - plugin: default_value

  17. default_value: 'blog_categories'

  18. destination:

  19. plugin: entity:taxonomy_term

  20. default_bundle: blog_categories

  21. migration_dependencies:

  22. required: {}

  23. optional: {}

Check for the value in an entity hook

  1. /**

  2.  * Implements hook_entity_update().

  3.  */

  4. function my_module_entity_update(Drupal\Core\Entity\EntityInterface $entity) {

  5. if (isset($entity->_migration) && $entity->_migration === 'blog_categories') {
  6. return;

  7. }

  8. // Some undesired custom hook logic.

  9. }

In this case the hook will never fire for this specific migration, but may fire for other migrations. Skipping the second condition will make sure the hook will never fire for migrations where the _migration dummy field is defined.

Mar 12 2019
Mar 12

This is a technical description of the 3.x branch of the module. For the newer 4.x branch, see this article.

Simple XML sitemap 3.1 has been released

The third major version of simple_sitemap has been long in the making bringing a more reliable generation process, a significantly more versatile API and many new functionalities. The first minor ugrade of the 3.x branch comes with views support and human readable sitemaps.

Major new features in 3.1

Simple XML Sitemap views supportViews and views arguments support

Including view display URLs in the sitemap has been possible through adding these URLs as custom links in the UI (or via the API).

View variations created by view arguments however are tedious to include as one would have to include every version of the URL.

The integration of the simple_sitemap_views inside simple_sitemap 3.x makes it easily doable via the UI.

Thanks to @WalkingDexter for his tremendous work on this submodule!

Human-readable sitemaps with XSL stylesheets

Before:

Sitemap without XSL

Now:

XML sitemap with XSL stylesheet

This will not change how bots interact with the sitemaps, it will however make the sitemaps readable and sortable for humans. This can be helpful when debugging the sitemap content or using the sitemap to visually present content to users.

Other improvements

You can see the list of bug fixess and improvements on the module's release page.

Upgrade path

The module upgrades fine from any of the 2.x and 3.x versions.

To upgrade the module via composer, $ composer require 'drupal/simple_sitemap:^3.1' can be used. Afterwards, just visit /update.php, or run $ drush updb to update the module's storage.

For more information about the 3.x branch of the module, see this post. I invite you to watch this space for a more in-depth technical tutorial on how to programmatically create sitemap types. Also feel free to leave a comment below!

Nov 18 2018
Nov 18

This is a technical description of the 3.x branch of the module. For the newer 4.x branch, see this article.

Simple XML sitemap 3.0 has been released

The third major version of simple_sitemap has been seven months in the making. The module has been rewritten from the ground up and now features a more reliable generation process, a significantly more versatile API and many new functionalities.

Major new features

Ability to create any type of sitemap via plugins

The 8.x-3.x release allows not only to customize the URL generation through URL generator plugins as 2.x did, but also creating custom sitemap types that mix and match a sitemap generator along with several URL generators to create any type of sitemap.

This 3-plugin system coupled with the new concept of sitemap variants makes it possible to run several types of sitemaps on a single Drupal instance. Now e.g a Google news sitemap can coexist with your hreflang sitemap.

A sitemap variant can but does not need to be coupled to entity types/bundles. When creating a sitemap generator, one can define where the content source is located and what to do with it upon sitemap generation/deletion.

Ability to create sitemap variants of various sitemap types via the UI

In 3.x links form a specific entity bundle can be indexed in a specific sitemap variant with its own URL. This means, that apart from /sitemap.xml, there can be e.g

  • /products/sitemap.xml,
  • /files/sitemap.xml or
  • /news/sitemap.xml.

All of these can be completely different sitemap types linking to Drupal entities, external resources. or both. They could also be indexing other sitemaps. The name, label and weight of each variant can also be set in the UI.

Sitemap statusNo more out of memory/time errors

While the 2.x version of the Simple XML sitemap module has been appreciated for its simple generation process, that process sometimes failed because of timeouts and memory limits on huge sites in limited environments.

In order to address that, the generation process has now been streamlined to using a single queue regardless of whether batch generation is being used, or backend (cron/drush) processes. Setting a time limit on the cron generation and limiting the amount of links per sitemap allows for hundreds of thousands of entities/elements being indexed hopefully without memory errors.

If a problem occurs, the module resumes the generation process by picking up the last indexed element. Another thing the new version does better is making the old sitemap version accessible during its regeneration. This way there is never a time where bots cannot index stuff.

Other improvements

There are many more improvements compared to 2.x. You can see the list of changes on the module's release page.

Upgrade path

It was a challenge to do all of the above while still maintaining a smooth upgrade process, but it was worth it. The module upgrades fine from any of the 2.x versions.

To upgrade the module via composer, $ composer require 'drupal/simple_sitemap:^3.0' can be used. Afterwards, just visit /update.php, or run $ drush updb to update the module's storage.

Please bear in mind, that the module's API has undergone several changes, so if you are calling it in your code, the method calls may need some adjustment. Check out the code documentation and the readme file.

Should you upgrade?

A small portion of the 35k+ sites that are using this module have been rocking the 3.x development version and helping out by reporting issues. All of the bugs have been ironed out leaving a clean bug queue. I believe the 3.x branch to be stable and given its robustness, its enhancements and the smooth upgrade path, I encourage the rest of you to upgrade to the most recent release.

3.0 is great, but what is coming in 3.1?

Views support

Including view display URLs in the sitemap has been possible through adding these URLs as custom links in the UI (or via the API). View display variants however (most of the time views with arguments) are tedious to include as one would have to include every version of the URL. The integration of the simple_sitemap_views inside simple_sitemap 3.x will make this easily doable via the UI.

Thanks to @WalkingDexter for his tremendous work on this submodule!

XSL stylesheets

This will not change how bots interact with the sitemaps, it will however make the sitemaps readable and sortable for humans. This can be helpful when debugging the sitemap content or using the sitemap to visually present content to users.

I invite you to watch this space for a more in-depth technical tutorial on how to programmatically create sitemap types. Also feel free to leave a comment below!

Aug 13 2018
Aug 13

Apparently there are still pretty common Drupal 8 theming tasks that cannot be accomplished with the great twig_tweak module. This by the way was me giving a plug to a great little module, which makes half of all your theme preprocess hooks unnecessary.

Update: Apparently there is a module like twig_tweak but with the ability to do the above. It is called bamboo_twig and its documentation can be found here - thanks to Luckhardt Labs for mentioning it. Mind you I have not tested it yet. There is a rather interesting issue in its queue about the lack of collaboration between the two module maintainers.

If you would like to get the URL from an image that is trapped inside of a media entity however, you can either extract it using the aforementioned preprocess function like so:

  1. function mytheme_preprocess_node(&$variables) {

  2. /** @var \Drupal\node\NodeInterface $node */

  3. $node = $variables['node'];

  4. $image_field = $node->get('field_background_image');

  5. if (!$image_field->isEmpty()) {

  6. $uri = $image_field->entity->get('field_media_image')->entity->uri->value;

  7. $variables['background_image_url'] = file_create_url($uri);

  8. }

  9. }

In the node template, you can display it using

  1. {{ background_image_url }}

... or use this nifty snipplet inside of your twig template directly:

  1. {% if node.field_background_image is not empty %}
  2. {{ file_url(node.field_background_image.entity.field_media_image.entity.fileuri) }}

In this case the media image URL is acquired from a node inside of a node tempalate node--node-type.html.twig, but this will work for other entities in other templates as well, e.g a paragraph in paragraph--paragraph-type.html.twig.

Mar 30 2018
Mar 30

Creating a duplicate of an entity

Creating a duplicate of an entity is easily done via the entity API method Entity::createDuplicate(). This is a convenient method if the goal is to clone an entity into a new entity, as all identifiers of the previous entity get unset when using this method.

  1. $nid = 5;

  2. $entity = \Drupal::service('entity_type.manager')->getStorage('node')->load($nid); // Use dependency injection instead if in class context.

  3. $duplicate = $entity->createDuplicate();

  4. $duplicate->save();

Cloning data into an existing entity

However partially or fully cloning data into an existing entity is less straight forward (and rightfully so). Still, the ability to do so can be useful

  • in custom migration scripts where we want to overwrite old entities without creating new ones,
  • in cases where we need to overwrite the old entity as other internal or external data may reference it and creating a new entity would break these references.

The second case could be an entity reference field referencing the old entity in question (this could be technically solved by reassigning the reference), but it could also be 3rd party software referencing the old entity, which would complicate things.

This article is going to demonstrate a couple of possible ways of cloning entity data into existing entities.

Cloning field data 1:1

  1. $source_nid = 5;

  2. $destination_nid = 6;

  3. // Use dependency injection in class context instead.

  4. $source = \Drupal::service('entity_type.manager')->getStorage('node')->load($source_nid);

  5. $destination = \Drupal::service('entity_type.manager')->getStorage('node')->load($destination_nid);

  6. foreach ($source->getFields() as $name => $field) {

  7. $destination->set($name, $field->getValue());

  8. }

  9. $destination->save();

Importing new data only

To import new field data without overwriting existing data, just check if the destination field is empty before cloning into it like so:

  1. foreach ($source->getFields() as $name => $field) {

  2. if ($destination->get($name)->isEmpty()) {

  3. $destination->set($name, $field->getValue());

  4. }

  5. }

Putting it all together

A nifty method that could be used to clone field data of entities into other existing entitites could look like this:

  1. /**

  2.  * @param \Drupal\Core\Entity\Entity $source

  3.  * @param \Drupal\Core\Entity\Entity $destination

  4.  * @param string $mode

  5.  * Can be 'keep', 'overwrite' and 'clone'.

  6.  * @param array $skip_fields

  7.  * An array of fields not to be cloned into the destination entity.

  8.  */

  9. public function cloneFields(Entity $source, Entity &$destination, $mode, $skip_fields = []) {

  10. foreach ($source->getFields() as $name => $field) {

  11. // In this case clone only fields and leave out properties like title.

  12. if (strpos($name, 'field') === 0
  13. // Leave out certain fields.

  14. switch ($mode) {

  15. // Import only those fields from source that are empty in destination.

  16. case 'keep':

  17. default:

  18. if (!$destination->get($name)->isEmpty()) {

  19. continue 2;

  20. }

  21. break;

  22. // Import field data from source overwriting all destination fields.

  23. // Do not empty fields in destination if they are empty in source.

  24. case 'overwrite':

  25. if ($source->get($name)->isEmpty()) {

  26. continue 2;

  27. }

  28. break;

  29. // Import field data from source overwriting all destination fields.

  30. // Empty fields in destination if they are empty in source.

  31. case 'clone':

  32. break;

  33. }

  34. $destination->set($name, $field->getValue());

  35. }

  36. }

  37. $destination->save();

  38. }

There you go. Make sure to comment below in case of questions or if you know a better way of doing the above.

Oct 13 2017
Oct 13

Upgrading to Drush 9

Drush should be installed and updated through composer. There is no stable Drush 9 version yet, so the development version must be used. Updating to the development version of Drush 9 is a simple as typing:

$ composer require drush/drush:dev-master

Porting your Drush commands to Drush 9

Porting the commands is a semi-automatic process: There is a command that will generate the required files and class structure for you. To start the wizard, just type:

$ drush generate drush-command-file -l dev

Drush will ask you for the module's machine name and for the optional path to the legacy Drush command file (the one that has your commands, ending with .drush.inc). You will have to provide the absolute path.

drush.services.yml

This is the file your Drush command definition goes into. Do not use your module's regular services.yml as you might have done in Drush 8 or else you will confuse the legacy Drush which will lead to a PHP error like this:

Fatal error: Class 'Drush\Commands\DrushCommands' not found in MyModuleCommands.

Use the dedicated drush.services.yml file in your module's root directory instead.

The file should look like this:

  1. services:

  2. mymodule.commands:

  3. class: \Drupal\mymodule\Commands\MyModuleCommands

  4. tags:

  5. - { name: drush.command }

As in other symfony service definitions, you can (and should) provide other services as arguments DI style and do all the other crazy stuff.

The most recent Drush 9 version recommends to explicitly declare the location of the drush command file for each version of drush by adding the extra.drush.services section to the composer.json file of the implementing module. This is now optional, but will be required for Drush 10.

To comply, let us declare the above file in composer.json for Drush 9:

  1. "extra": {

  2. "drush": {

  3. "services": {

  4. "drush.services.yml": "^9"

  5. }

  6. }

  7. }

Refusing to alter composer.json will result in the following message while running drush commands:

module_name should have an extra.drush.services section. In the future, this will be required in order to use this Drush extension.

MyModuleCommands.php

  1. namespace Drupal\mymodule\Commands;

  2. use Drush\Commands\DrushCommands;

  3. /**

  4.  *

  5.  * In addition to a commandfile like this one, you need a drush.services.yml

  6.  * in root of your module.

  7.  *

  8.  * See these files for an example of injecting Drupal services:

  9.  * - http://cgit.drupalcode.org/devel/tree/src/Commands/DevelCommands.php

  10.  * - http://cgit.drupalcode.org/devel/tree/drush.services.yml

  11.  */

  12. class MyModuleCommands extends DrushCommands {

  13. /**

  14.   * @command mymodule:do-something

  15.   * @param array $options An associative array of options whose values come from cli, aliases, config, etc.

  16.   * @validate-module-enabled mymodule

  17.   * @aliases mm:do-something, mm:ds, mymodule-do-something

  18.   */

  19. public function generate()

  20. {

  21. // See bottom of https://weitzman.github.io/blog/port-to-drush9 for details on what to change when porting a

  22. // legacy command.

  23. }

  24. }

As seen above, the generate() method needs to be implemented manually. Other manual changes may include creating a constructor in case other services are injected.

Drush 9 mimics symfony's style module:command naming structure and this should be respected. I don't see any reson not to include the legacy command as an alias however: If your command used to be my_module:do-something, use my-module:do-something in @command, but also the old my_module-do-something as @alias as presented in the example above. This way scripts calling the old Drush will continue working.

Maintaining Drush 8, Drush 9 and Drupal Console commands side by side

The new three standards of managing Drupal through a shell should not be an excuse for bad practice. To avoid code duplication, make sure your module defines a service which holds all the business logic that can be run by any of the above tools.

Simple XML Sitemap (project page) now supports Drush 9 and is a good example of this principle:

simple_sitemap.drush.inc (Drush 8)

  1. /**

  2.  * @file

  3.  * Drush (< 9) integration.

  4.  */

  5. /**

  6.  * Implements hook_drush_command().

  7.  */

  8. function simple_sitemap_drush_command() {

  9. $items['simple-sitemap-generate'] = [

  10. 'description' => 'Regenerate the XML sitemaps according to the module settings.',

  11. 'callback' => 'drush_simple_sitemap_generate',

  12. 'drupal dependencies' => ['simple_sitemap'],

  13. 'aliases' => ['ssg'],

  14. ];

  15. $items['simple-sitemap-rebuild-queue'] = [

  16. 'description' => 'Rebuild the sitemap queue for all sitemap variants.',

  17. 'callback' => 'drush_simple_sitemap_rebuild_queue',

  18. 'drupal dependencies' => ['simple_sitemap'],

  19. 'aliases' => ['ssr'],

  20. ];

  21. return $items;

  22. }

  23. /**

  24.  * Callback function for hook_drush_command().

  25.  *

  26.  * Regenerate the XML sitemaps according to the module settings.

  27.  */

  28. function drush_simple_sitemap_generate() {

  29. \Drupal::service('simple_sitemap.generator')->generateSitemap('drush');

  30. }

  31. /**

  32.  * Callback function for hook_drush_command().

  33.  *

  34.  * Rebuild the sitemap queue for all sitemap variants.

  35.  */

  36. function drush_simple_sitemap_rebuild_queue() {

  37. \Drupal::service('simple_sitemap.generator')->rebuildQueue();

  38. }

SimplesitemapCommands.php (Drush 9)

  1. namespace Drupal\simple_sitemap\Commands;

  2. use Drupal\simple_sitemap\Simplesitemap;

  3. use Drush\Commands\DrushCommands;

  4. /**

  5.  * Class SimplesitemapCommands

  6.  * @package Drupal\simple_sitemap\Commands

  7.  */

  8. class SimplesitemapCommands extends DrushCommands {

  9. /**

  10.   * @var \Drupal\simple_sitemap\Simplesitemap

  11.   */

  12. protected $generator;

  13. /**

  14.   * SimplesitemapCommands constructor.

  15.   * @param \Drupal\simple_sitemap\Simplesitemap $generator

  16.   */

  17. public function __construct(Simplesitemap $generator) {

  18. $this->generator = $generator;

  19. }

  20. /**

  21.   * Regenerate the XML sitemaps according to the module settings.

  22.   *

  23.   * @command simple-sitemap:generate

  24.   *

  25.   * @usage drush simple-sitemap:generate

  26.   * Regenerate the XML sitemaps according to the module settings.

  27.   *

  28.   * @validate-module-enabled simple_sitemap

  29.   *

  30.   * @aliases ssg, simple-sitemap-generate

  31.   */

  32. public function generate() {

  33. $this->generator->generateSitemap('drush');

  34. }

  35. /**

  36.   * Rebuild the sitemap queue for all sitemap variants.

  37.   *

  38.   * @command simple-sitemap:rebuild-queue

  39.   *

  40.   * @usage drush simple-sitemap:rebuild-queue

  41.   * Rebuild the sitemap queue for all sitemap variants.

  42.   *

  43.   * @validate-module-enabled simple_sitemap

  44.   *

  45.   * @aliases ssr, simple-sitemap-rebuild-queue

  46.   */

  47. public function rebuildQueue() {

  48. $this->generator->rebuildQueue();

  49. }

  50. }

drush.services.yml (Drush 9)

  1. services:

  2. simple_sitemap.commands:

  3. class: \Drupal\simple_sitemap\Commands\SimplesitemapCommands

  4. arguments:

  5. - '@simple_sitemap.generator'

  6. tags:

  7. - { name: drush.command }

All of the business logic of this command is inside of the method generateSitemap() of the simple_sitemap service.

Downgrading back to Drush 8

Not a fan of changing APIs? Downgrading is a composer command away:

$ composer require drush/drush:^8.0

Conclusion

It is good to see the Drush project keeping up with time and pubishing Drush 9 parallely to the appearance of Drupal 8.4.0. The API changes are the necessary price we pay for a modern and continuously evolving framework like Drupal.

Feel free to leave a comment below in case of questions or new Drupal 8.4 / Drush 9 insights.

Sep 25 2017
Sep 25

This is a technical description of the 2.x branch of the module. For the newer 3.x branch see this article; for 4.x, see this article.

New features of Simple XML sitemap

Version 2.10 of Simple XML sitemap is mainly a feature release with only a few minor bugs fixed. The new features are

  • the implementation of the changefreq parameter
  • the ability to set an interval at which to regenerate the sitemap
  • the ability to customize XML output
  • the ability to add arbitrary links to the sitemap
  • image indexation

See the 8.x-2.10 release page for details.
A new version has been released, please make sure to visit the project page.

Image indexationInclusion settings

Simple XML sitemap is now able to create Google image sitemaps through indexing all images attached to entities. This includes images uploaded through the image field as well as inline images uploaded through the WYSIWYG. The inclusion of images can be set at the entity type and bundle level but can be overridden on a per-entity basis giving you all the flexibility.

Please bear in mind, that all images attached to entities get indexed regardless of their file system location (public/private). Another thing worth noting is that the original images get indexed, not the derived styles. This should be considered before indexing entities with many high resolution images which could increase traffic.

Indexation of custom link images has not made it into this release, but the feature is already available in the development version of the module.

Adding arbitrary links to the sitemap

Most use cases dictate the inclusion of internal links which can be achieved through adding entity links to the index. For non-entity pages like views, there has been the possibility to add custom links through the UI or the API. In both cases however the system only allows internal links which are accessible to anonymous users. The new version of the module provides a way to add any link to the index, even ones Drupal does not know about:

  1. /**

  2.  * Use this hook to add arbitrary links to the sitemap.

  3.  *

  4.  * @param array &$arbitrary_links

  5.  */

  6. function hook_simple_sitemap_arbitrary_links_alter(&$arbitrary_links) {

  7. // Add an arbitrary link.

  8. $arbitrary_links[] = [

  9. 'url' => 'http://example.com',

  10. 'priority' => '0.5',

  11. 'lastmod' => '2012-10-12T17:40:30+02:00',

  12. 'changefreq' => 'weekly',

  13. 'images' => [

  14. ['path' =>'http://path-to-image.png']

  15. ]

  16. ];

  17. }

As the example shows, all properties of the link like priority/lastmod/changefreq can be defined as well.

To alter links shortly before they get transformed to XML output, there is still the possibility to use the following:

  1. /**

  2.  * Alter the generated link data before the sitemap is saved.

  3.  * This hook gets invoked for every sitemap chunk generated.

  4.  *

  5.  * @param array &$links

  6.  * Array containing multilingual links generated for each path to be indexed.

  7.  */

  8. function hook_simple_sitemap_links_alter(&$links) {

  9. // Remove German URL for a certain path in the hreflang sitemap.

  10. foreach ($links as $key => $link) {

  11. if ($link['path'] === 'node/1') {

  12. // Remove 'loc' URL if it points to a german site.

  13. if ($link['langcode'] === 'de') {

  14. }

  15. // If this 'loc' URL points to a non-german site, make sure to remove

  16. // its german alternate URL.

  17. else {

  18. if ($link['alternate_urls']['de']) {

  19. unset($links[$key]['alternate_urls']['de']);
  20. }

  21. }

  22. }

  23. }

  24. }

Basic alteration of the XML output

The following two new hooks can now be used to alter the XML output:

  1. /**

  2.  * Alters the sitemap attributes shortly before XML document generation.

  3.  * Attributes can be added, changed and removed.

  4.  *

  5.  * @param array &$attributes

  6.  */

  7. function hook_simple_sitemap_attributes_alter(&$attributes) {

  8. // Remove the xhtml attribute e.g. if no xhtml sitemap elements are present.

  9. unset($attributes['xmlns:xhtml']);
  10. }

  11. /**

  12.  * Alters attributes of the sitemap index. shortly before XML document generation.

  13.  * Attributes can be added, changed and removed.

  14.  *

  15.  * @param array &$index_attributes

  16.  */

  17. function hook_simple_sitemap_index_attributes_alter(&$index_attributes) {

  18. // Add some attribute to the sitemap index.

  19. $index_attributes['name'] = 'value';

  20. }

Other API changes

The API is now more forgiving allowing missing link setting arguments when using some of its inclusion altering methods. Here is en example of the simple_sitemap.generator API in action:

  1. \Drupal::service('simple_sitemap.generator')

  2. ->saveSetting('remove_duplicates', TRUE)

  3. ->enableEntityType('node')

  4. ->setBundleSettings('node', 'page', ['index' => TRUE, 'priority' => 0.5])

  5. ->removeCustomLinks()

  6. ->addCustomLink('/some/view/page', ['priority' => 0.5])

  7. ->generateSitemap();

More documentation can be found here. I hope the new version of this module will be of great use to you!

Feb 23 2017
Feb 23

Since its relaunch in 2015, the Drupal 7 powered precore.net has been gaining popularity among artists and design students to become their go-to platform. Until today, design students have uploaded over 700 portfolios providing guidance to enrolling candidates. These portfolios are linked to over 500 art faculties of hundreds of universities.

Before enrolling in a course, a candidate can research their local university and study other students' portfolios or enroll in their local design course to prepare for the entry tests - all of it on precore.net.

On top of that, students provide and collect support on the precore.net forum which boasts over 20000 users who have written nearly 250000 posts. This may be the biggest and most beautiful forum built on top of Drupal core.Frontpage

The most powerful feature however may be the ability for guests to create most of the site's content without having to go through any type of registration process. Visitors can go ahead and correct their school's information just by clicking 'edit'. Likewise, anyone can write a blog post - no account or personal information needed. We think this technology has massively contributed to the quantity and quality of content on precore.net.

While the numbers of design students, universities and art schools registering with the platform has been growing steadily, the visionaries behind the project, Ingo Rauth and Wolfgang Zeh from projektgestalten, recently decided to take the platform to the next level by bringing it to jobseekers and providers as well. Consequently gbyte has implemented the event functionality and the new job board.

This is not going to be the last improvement though, apparently artists have lots of creative ideas and we look forward to implementing them. We feel that this project is a great showcase of Drupal's possibilities and if you would like to learn more about the project or its implementation, make sure to leave a comment below or contact us via the contact form.

Check out other technology-centric posts about the project as well as more screenshots on the project page.

Jan 22 2017
Jan 22

Letting users create content without having to register (or going through any other annoying process) is becoming an important customer engagement strategy.

When you allow anonymous users to create content on your website, you want this content to go through a moderation process before it becomes publicly available. To implement this in Drupal, the anonymous user has to be given permission to create content and the content type needs to be unpublished by default.

The problem with Drupal 7 and Drupal 8 is that as soon as the anonymous user saves new content, they loose access rights to it and get redirected to an 'Access denied' page which is not very user friendly.

In addition to the above, you may want the anonymous user to be able to edit or even delete their own content in case they find an error right after submitting it. Users often find typos or other kinds of mistakes right after content submission.

Gbyte created the Session Node Access module to tackle exactly these issues. The module allows administrators to grant certain user roles (not only anonymous users) specific permissions to content they created. These permissions last only as long as the browsing session lasts; after that, the regular permissions apply again. This way it is possible to allow guests or users of a certain role to keep access to their content, even if it is pending for approval.

Now Session Node Access has been ported to Drupal 8 - thank you to Gaël Gosset for doing the initial porting.

Right now this module works only with nodes, we may implement it for other entities in case of demand.

Feel free to download Session Node Access from its module page.

Session Node Access configuration screen:

Session Node Access configuration screen

Feb 28 2016
Feb 28

This is a technical comparison of the older 2.x branch of the Simple XML sitemap module and an older development version of XML sitemap. XML sitemap has just had its first release and for more on the newer 4.x branch of Simple XML sitemap see this article.

This comparison may be interesting for XML sitemap users moving to Drupal 8 or for users intending to wait for the port. (Do not wait, help out!) Please also be advised that gbyte made Simple XML sitemap, which makes this comparison intrinsically biased.

There are major differences between Simple XML sitemap (simple_sitemap) and XML sitemap (xmlsitemap) and depending on your use case, you might want to choose one or the other.

What sets the modules apart are their complexity, extensibility, performance and feature sets.

Code base

Having been built specifically for D8, simple_sitemap has arguably a cleaner code base adhering to D8 standards i (i.e. use of OOP). In contrast, the xmlsitemap module will have a hard time adjusting to D8 technologies and guidelines, as it carries around a whole lot of legacy code going back as far as Drupal 5.

Performance

What is meant here is the impact of the module on a Drupal 8 system, how quick the sitemap generation process is and how long it takes for a visitor to fetch the sitemap.

Sitemap generation performance

Both the modules feature the batch API which allows to generate huge amounts of links without hitting any PHP limits. The sitemap generation performance differs in that it is a one step process in simple_sitemap as opposed to the two step process of the xmlsitemap module.

While the simple_sitemap's one step process is less error prone and initially generates the sitemap quicker, xmlsitemap's two steps have the advantage of tracking which entities have changed since last generation through implementing an additional database table making sure the subsequent rebuilds are quicker.

Sitemap fetching performance

When it comes to fetching the sitemap, both modules cache them: xmlsitemap creates a physical file, while simple_sitemap caches the sitemap in the database.
Both modules have the ability to set the maximum amount of links included in the sitemap and they generate multiple sitemaps along with an index if this amount is exceeded.

Consequently there should be no noticeable difference between the two modules when it comes to fetching performance.

Overall system impact

Because of its leaner code base and the fact that the code does not get invoked through hooks all to often, simple_sitemap's footprint is smaller.

Supported entities

Both of the modules support all core content entity types like nodes, taxonomy terms, menu links, users, etc. as well as contributed entity types (e.g. commerce products or media) out of the box. This is possible due to the great D8 entity API. Whereas xmlsitemap keeps its bundle settings on a designated page, simple_sitemap incorporates its bundle settings into bundle edit pages, which seems a bit more intuitive. Both modules allow overriding of sitemap settings on a per-entity basis.

XML Sitemap standards

Here the edge goes to simple_sitemap, as it features the newer hreflang XML standard developed by google. In addition simple_sitemap is optionally able to index images attached to an entity resulting in an image sitemap. Adhering to Google's standards is important, as this is the search engine most of us would like to correctly understand and index our site.

Additional functionality

Having been around since 2007, the xmlsitemap module has had lots of time to incorporate various additional functionalities which are not present in simple_sitemap yet. Automatically sending the stiemap to search engines is an example of such a functionality.

Extensibility

Whereas xmlsitemap offers many hooks making it easy to alter the XML output, simple_sitemap's strength lies within its powerful service API allowing to chain tasks like adding custom links and altering configuration. Since version 2.11, simple_sitemap takes advantage of plugins, so new URL generators can be implemented by 3rd party modules. Depending on your needs, you may find one approach superior to the other.

Which one is right for me?

As of now, simple_sitemap is the more stable module having close to none open bug reports. As soon as xmlsitemap runs well in Drupal 8 however, you will have to decide: simple_sitemap for a more performant codebase with the newer sitemap standard and more powerful API, or xmlsitemap for its bigger feature set and a smarter sitemap generation process.

Oct 11 2015
Oct 11

Today's users are becoming increasingly spoiled by technologies allowing them to deeply interact with websites without having to create an account first. To keep up with this development and to entice users to use your website without them having to give up any personal information requires a bit of problem solving in Drupal.

The first obvious problem to solve is the increase of spam posts this set up will inevitably cause. If your site is small to medium sized, I recommend the Honeypot module configured the way it's described in this article. For huge sites, captcha will be the only way to go.

The other thing is that you probably do not want the anonymous content to be publicly visible after creation. The content must first go through an administrative approval process before it is publicly available. In both Drupal 7 and Drupal 8 you can do exactly this by unsetting the default value of the 'published' checkbox on the content type edit page.
The problem with this is that as soon as the anonymous user saves the content, they loose access rights to it and get redirected to an 'Access denied' page which is not very user friendly.

In addition to the above, you may want the anonymous user to be able to edit or even delete their own content in case they find an error right after submitting it. Users often find typos or other kinds of mistakes right after content submission.

Gbyte created the Session Node Access module to tackle exactly these issues. The module allows administrators to grant certain user roles (not only anonymous users) specific permissions to content they created. These permissions last only as long as the browsing session lasts; after that, the regular permissions apply again. This way it is possible to allow guests or users of a certain role to keep access to their content, even if it is pending for approval.

Feel free to download Session Node Access from its module page.

Session Node Access configuration screen:

Session node access configuration screen

Oct 02 2015
Oct 02

The Simple XML Sitemap module was originally created by gbyte as a temporary replacement for the non-functioning Drupal 8 XML Sitemamp Module. After putting some more work into it however, we decided to keep using it in our D8 & D9 projects, as it is very lightweight, simple to use and most importandly, adheres to a newer XML sitemap standard.

The 2.x branch features most of the functionality of the heavier XML Sitemap module while also featuring hreflang and image XML sitemaps, which is a new Google standard for creating multilingual XML sitemaps that should improve SEO even more.

The 3.x branch adds a robust generation process that works with huge sites in limited environments. It also introduces the concept of sitemap variants which are instances of different sitemap types which in turn are made of sitemap and URL generators. This makes it possible to run several different sitemap types on one Drupal instance.

The 4.x branch is a rewrite with developers/integrators in mind and makes much greater use of Drupal's entity API dropping some of its very specific chaining API. On top of that, it features many code and UI improvements.

Here is the description from the module page:

Every webpage needs an automatic XML sitemap generator for SEO reasons. Sitemaps generated by this module adhere to the new Google standard regarding multilingual content by creating hreflang sitemaps and image sitemaps - Googlebots will thank you later.

In addition to the default hreflang sitemaps, the module's API allows creating and publishing of custom sitemaps with arbitrary content.

Functionality

The module generates a multilingual sitemap for entities, views and custom links. Out of the box it supports most of Drupal's content entity types including:

  • nodes
  • taxonomy terms
  • menu links
  • users
  • ...

Contributed entity types like commerce products can be indexed as well. Various inclusion settings can be set for bundles and overridden on a per-entity basis. Sitemap generation can be altered through custom URL generator plugins and hooks. The sitemaps can be automatically submitted to search engines.

Here is a sample of the markup it generates (press ctrl+u to view the source).

8.x-3.x

Ability to create any type of sitemap via plugins

The 8.x-3.x release allows not only for customizing the URL generation through UrlGenerator plugins as 2.x does, but also creating multiple custom sitemap types through sitemapGenerator plugins and running all the sitemaps on the same Drupal instance. Now e.g a Google news sitemap can be added to a Drupal instance. This is possible through the new concept of sitemap variants.

Ability to create sitemap variants of various sitemap types via UI

Now e.g links form a specific entity bundle can be indexed in a specific sitemap variant with its own URL.

No more out of memory/time errors

The generation process has been streamlined to using a single queue regardless of whether batch generation is being used, or backend (cron/drush) processes. This should allow hundreds of thousands of entities/elements being indexed without memory errors.
If there is a problem, the generation process picks up from the last indexed element. The sitemap variants are only published after the generation has been completed.

Other

  • Automatic submission to search engines
  • Views and views arguments support
  • XSL stylesheets for human visitors
  • Performance test script included

4.x

Module rewrite with developers/integrators in mind

4.x makes much greater use of Drupal's entity API dropping some of its very specific chaining API. See #3219383: Roadmap for 4.x.

See this post for more details on the 4.x branch.

Should you use this over 3.x?

Yes. New features are only coming to 4.x.

Upgrade path

Please do not forget to run drush updb or update.php after every update. If you get an error, run core/rebuild.php before the above.

Keep in mind, the module APIs change between major releases.

  • Branch 8.x-1.x is no longer supported and there is no upgrade path.
  • You can upgrade from any 8.x-2.x version to 8.x-3.x or (preferably) to 4.x.

Other

Similar modules: XML Sitemap was the de facto XML sitemap generator prior to Drupal 8 and a stable version for D8 has just been released.

Feel free to grab the module from the module page.

If you are unsure whether to get simple_sitemap or xmlsitemap, check out this comparison on the two modules.

This article describes all the new features of the 4.x version.

Oct 01 2015
Oct 01


Please note the article's publishing date. Some of the information presented below may not be current anymore.

With only a few critical issues left in the Drupal 8 queue and D8 being surprisingly usable, many developers already use it in small projects to play with the technology and to challenge themselves.
I have to admit, I am no exception - the embracement of many PHP technologies and (finally!) the jump to the OOP paradigm makes me want to stop writing right now and code some more.

Which projects qualify for Drupal 8 today?

I would wait a few months before creating bigger D8 projects for my clients. The community has to play some catching up first and port modules, themes and write documentation. On top of that, apart from all the OOP technologies we love, there have been some new drupalisms introduced and not documented yet - this combined with the lack of contributed module solutions makes D8 development much more time consuming for paid projects in comparison to D7.

Small projects however are very doable.

First however, it may be necessary to upgrade the server, as D8 introduces relatively high PHP and SQL requirements. See the official requirements page.

With its translation capabilities, ckeditor and views in core, creating a simple portfolio or blog website with Drupal 8 may be even quicker than using its predecessor. The built in WYSIWYG even lets you upload files directly into it. A feature for which you had to install ocupload in the past.
On a side note, going through the list of modules, you may be negatively surprised by the fact that it is impossible to disable modules - this has been done by design, check out this page. The gray area where a module has its settings saved in the database but is still disabled, is gone.

Drupal 8 module creation

The small number of contributed modules will force you to create your own, even for small changes on the site. I cannot overstate how fulfilling it is to code Drupal modules and at the same time be able to (mostly) follow PHP best practices. The .module file feels almost like a thing from the past left there to accommodate some developers and site builders resisting change. It will include all the hook functions which will hopefully only call Drupal and custom class methods anyway. Those module class files is where the real action takes place!

Variables as means to store quick data have been replaced by the much more sophisticated configuration system. This move brings many advantages including data portability, an actual relation between the module and its data (no more abandoned variables after module deletion) and many more.

Finally Caching has become smart! Cached content is able to invalidate itself depending on several circumstances, including content changes. Setting cache tags for cached module data is very easy and efficient. This system makes the heavy Drupal technology appear fast on an interpreted language like PHP.

Heads up regarding the devel module: it installs fine, however dpm() runs out of memory and leads to a WSOD, regardless of the amount of memory set. The krumo() function (or better yet your debugger) is a working alternative.

Drupal 8 template creation

You will probably find that there is no real choice in templates and that creating a custom theme is a must. Creating sub themes in Drupal is a treat. TWIG for the markup, YAML for the config files, it all fits rather well together.

For most projects you will be creating a sub theme on top of the included 'classy' theme or another contrib base theme. Adding CSS and JS files and combining them to libraries in the .libraries.yml file provides a lot of flexibility more or less deprecating the D7 libraries module.

Overriding a base theme is really simple by adding specifically named templates, including and excluding(!) certain base theme libraries.

The state of Drupal 8 SEO and spam prevention modules

While Drupal 8 comes with a well balanced selection of  core modules, there is a couple of functionalities which will be added on top in most of your projects. These are spam prevention and SEO. Drupal 7 has a ton of modules for these purposes while Drupal 8... not so much.

In terms of spam prevention, for smaller to medium projects, I recommend the dev version of the Honeypot module. See How to use the Drupal 8 honeypot module efficiently. For big projects, captcha seems to be a good choice, however currently it has some major issues.

In terms of SEO, the metatag module is far from functional (do not bother to download the dev version, it is just some test code displaying a phone number field), but this is not tragic, the metatag module's impact on SEO has been declining a lot anyway.

The real problem has been the lack of an xml sitemap generator. Google loves sitemaps, but the Drupal 8 version of the xml sitemap module is broken. This is why I created the Simple XML Sitemap. It is stable and works well with the latest beta RC release. Feel free to read about it here, or download the module directly from the Simple XML Sitemap module page.

Get ready to work around some issues

As mentioned, the documentation is really lacking, but we have to work with what we have. While everyone is focusing on the critical issues, do keep an eye on the other ones as well. From the top of my head I have experienced D8 issues like

  • css caching problems
  • language detection problems
  • self-resetting menu items
  • broken views contextual filters
  • incomplete entityQuery class
  • missing private file system image styles

... many of those still prevail until beta 15, which is supposed to be the second last beta release. I guess the only thing to do is contributing to the issue queues.

All in all though the experience has been rewarding and I strongly encourage everyone to start hacking with Drupal 8. I am looking forward to completing tons of D8 projects soon. I invite you to share your experience in the comment section.

If you would like to port your website to Drupal 8, make sure to get in touch to get info on migration feasibility and techniques and to acquire a quote.

Sep 30 2015
Sep 30

The Drupal 7 translation system including the internationalization package is a heavy beast and while it mostly gets the job done, it is all but intuitive in use.

For high volume translations it is recommended to use the translation template extractor and translate the strings externally. For small corrections however, it is often much more convenient to use the translation interface (admin/config/regional/translate/translate).

Now that you've created that shiny module/template and made it translatable by passing all strings through the t() function, you may be wondering why your newly created string is not showing up in the translate interface.

To save you some trouble, here is a short list of things to check:

1. Include the project version number within the module/theme .info file.
Without the project version information, the translate interface will not register new translatable strings in your module/theme. Make sure to add version = 7.x-1.0 into the module/template .info file.

2. Run the string through the function in a non-standard language mode.
In order for the translation system to add your translatable string, the t() function must run at least once in a non-standard language. To achieve that, you will need to switch the language of the site to one you are going to translate into and then visit the page that displays the string.

3. Mind the case sensitive search filter.
Be accurate when using the translate interface filter - it does not forgive.

4. Flush caches if necessary.
Usually this is not needed, but maybe the new translatable string gets called on on a cached page or a cached view. With many layers of caching, better make sure and rule out all possible errors.

Let me know if anything is missing in this list.
Happy translating!

Sep 28 2015
Sep 28

The Honeypot module is a great captcha alternative, as it keeps spam bots from submitting content while also saving your site visitors from having to type in mundane character combinations.
Configured properly it will prevent the majority of bots from submitting forms on your site including registration forms, contact forms, comment forms, content forms... any drupal forms.
It works differently from Captcha: it lures the bot into filling out a form field invisible to regular users. By doing so, the system recognizes the bot for what it is and denies the submission.

While being very user friendly, this reversed bot detection system comes at the cost of some bot submissions getting through anyway. This is why I would advise against using this module on large sites, where it is difficult to track every piece of submitted content. It should work well for smaller and medium sites however, it has been working well in many of my projects including this very site.

Honeypot configuration

Correct configuration of the module is extremely important, as wrong settings might make the module inefficient or worse, prevent real users from submitting forms. After configuring the module, make sure to double check it works by submitting a protected form as an anonymous user.

Once installed and enabled, go to admin/config/content/honeypot to configure the module.

First of all careful with the "protect all forms" option, as caching will be disabled on every page that includes a form. This can be problematic in cases where e.g. a login block is embedded in the sidebar. In addition to ticking what forms to protect, there are two important settings to keep in mind.

"Honeypot time limit" sets an additional non-honeypot protection method which will assume, that a form submitted within the set amount of seconds after page load is submitted by a bot. Even though this option disables page caching, we found disabling it takes away from the module's effectiveness. Five seconds is a safe number for most cases, as human users will need more time to submit a form.

The other option is the "Honeypot element name" where the name of the honeypot form field can be set. Now some important advise: Do not use the default field name. Change it to something else. You can be creative and use age, sex, www, attractiveness and so on. We found using a different honeypot field name greatly improves bot detection. This is probably due to certain bots being preprogrammed to pass the drupal honeypots' "are you a bot" test.

At the beginning it also makes sense to check the logging checkbox lean back to learn how many submissions are being blocked by the module and possibly lock the ip addresses.

Honeypot in Drupal 8 & 9

The D8 branch of honeypot is very usable, however I recommend the development version (> 8.x-1.x-dev) for now. The stable version has some caching problems breaking the "time limit" function. The development version works very well though.

If you develop with Drupal 8, make sure to check out the article What to keep in mind when creating Drupal 8 projects - for developers.

It's been a while since this article was written and since then the module as well as the Drupal 8 & 9 platform have become stable tools.

Link to honeypot module page.

Feb 26 2015
Feb 26

If you need a simple Views display switch to toggle e.g between a list and a grid display of a view, there are a couple of plug & play options already.

Display Suite and Quick Tabs are modules which provide this functionality but they seem to be quite an overkill for a simple display switch. View modes on the other hand seems to be exactly what is needed, however there is no stable version and the development one did not work for me.

How it needs to work

Our use case dictates that while switching a display, the view needs to retain the exposed filter values and page number. The page will be reloaded, no AJAX magic here.

Views display switch

So let's create our own views display switch. In order to do that you will obviously be needing a view with at least two page displays showing content in different ways. You will also have to put some code into your custom module. If in doubt, refer to the countless other tutorials.

Set up your view

In the view named [view] set the path of [display_1] to e.g [page/grid], the path to [display_2] to e.g [page/list].

Create callback function

Create a simple callback function which will provide the switch in ready-to-be-displayed HTML.

  1. /**

  2.  * Gets HTML output of a switch which will switch between grid and list display of a view.

  3.  */

  4. function [mymodule]_get_views_display_switch() {

  5. $switch = l(t('Grid'), '[page/grid]', array(
  6. 'query' => drupal_get_query_parameters(), // This ensures the view will keep filter settings when switching the display.

  7. 'class' => array('page-grid-switch') // Adding a css class for this link.
  8. )

  9. ));

  10. $switch .= ' | ';

  11. $switch .= l(t('List'), '[page/list]', array(
  12. 'query' => drupal_get_query_parameters(),

  13. 'class' => array('page-list-switch')
  14. )

  15. ));

  16. // Adding CSS class for whole switch.

  17. $switch = "

    "</span> . $switch . "

    "
    ;
  18. return $switch;

  19. }

Implement views hook

Implement hook_views_pre_view hook to add the switch to the view.

  1. /**

  2.  * Implements hook_views_pre_view().

  3.  */

  4. function [mymodule]_views_pre_view(&$view, &$display_id, &$args) {

  5. if ($view->name == '[view]' && $display_id == '[display1]' || $display_id == '[display_2]') {

  6. // Adds a display switch to the header of a view.

  7. // 'footer' as second parameter will add the display switch to the footer of the view instead.

  8. $view->add_item($display_id, 'header', 'views', 'area', array('content' => [mymodule]_get_views_display_switch(), 'format' => 'full_html'));
  9. }

  10. }

This should do it. The l() function will make sure the link is marked active when it's active and drupal_get_query_parameters() makes sure the exposed filters and current page are retained while swichting.

Update

Apparently there is now a Drupal 8/9 module which implements these solutions: views_display_switch. I have not tested it, but have a go and let me know how well it works.

Nov 04 2014
Nov 04

Please note the date of the article - it may not be current nor does it necessarily reflect the author's current opinion on the matter. See this comment.

The ability to fork is a wonderful thing.

In the open source community, the ability to fork software projects is a wonderful thing, as it allows taking a software snapshot in a completely different direction from what was intended by its current maintainers.

Projects get forked for reasons that can be categorized in political (changing ownership rights, controversial decisions made by the project maintainers, etc.), technology related (where maintainers disagree about the direction of development and implementation) and personal.

Forking is a bad thing.

Wait... did you not just say forking was wonderful?
The ability to fork is wonderful, as it gives great power to the community. But forking itself is bad for the project, as it results in two projects with weaker development and support, a weakened potential to grow and a divided and confused user base. It leads not only to separate code bases, but also to a divided developer and user community and should be considered last resort.
In the best case scenario, forking is choosing the lesser evil.

No matter how much effort is put into collaboration between the fork and the original project, in the end it always ends in lack of compatibility and refusal to provide support to confused users in the different camp. This is why the Backdrop creators' reassuring statements about cross contribution should be taken with a grain of salt.

Why is forking Drupal into Backdrop a bad thing.

I will not spend much time summarizing the motivations behind Backdrop - go ahead and take a look here. The problem with these motivations is that, using my previous attempt to categorize reasons for forking, they are very technology related. In essence it seems the main reason is conservatism and fear of all the new things that come with Drupal 8:

Drupal 8 is using many new libraries and established technologies instead of further developing its own ones - bad.

Drupal 8 is utilizing OOP - what is it and who needs it? Writing loose functions in hooks has been working fine so far, why change that?

Drupal 8 is not backwards compatible (quelle surprise). Maybe I should have stayed with Wordpress. Nah, Iet's fork Drupal instead.

Drupal 8 forces established developers to learn something new. Obviously it's more convenient to do the same thing over and over instead.

Why fix if it ain't broke? I hear all the time.
In IT one should change something as soon as it is out of date instead of waiting until it breaks. Drupal has established itself as a bleeding edge CMF and bleeding edge often means utilizing new technologies, a scalable programming paradigm (hello OOP) and breaking backward compatibility. It also means that one has to relearn stuff sometimes. If I do not want to relearn stuff, I do not go into IT and certainly not Drupal.

Let me quote Dries' opinion on embracing change from his blog post:

The reason Drupal has been successful is because we always made big, forward-looking changes. It’s a cliché, but change has always been the only constant in Drupal. The result is that Drupal has stayed relevant, unlike nearly every other Open Source CMS over the years. The biggest risk for our project is that we don't embrace change.

It is impossible to assess how harmful a fork will be for the future of Drupal, if Backdrop succeeds in attracting even a small fraction of our developers and users. A lot of potential may be wasted on a project whose existence is not really justified. More probable however will be the fork's natural death.

What action should be taken instead?

When publishing your first Drupal module, it first has to go through a tough process, in which among other things, the community thoroughly tests whether it is duplicating functionality of other modules. Quotes get thrown at contributors like 'collaboration over competition'. The process takes a long time in which many projects are discarded and often rightfully so.

Dear creators of Backdrop, why not apply this 'collaboration over competition' philosophy to the bigger picture, and instead of forking Drupal, be more present in the discussion groups and steer the development of Drupal 9 into the direction you feel comfortable with? If you find that the bigger part of the community is not interested in having it your way, how about contributing an api module, so that the minority in question can profit? You can create a community around a module inside the Drupal ecosystem and be successful with it. If time shows your ideas to be of great value, they will be ported into core.

Backdrop is not about users, it is mainly about developers refusing to adapt and invest their time to further improve the Drupal platform. My appeal to them is not to hide behind claims like 'no backwards compatibility' or 'too complex to learn' and instead of dividing the community, start contributing to the bleeding edge Drupal platform we have learned to love.

Feel invited to discuss in the comment section!

Sep 08 2014
Sep 08

Why check if term is associated to a node prior to deletion?

In cases where taxonomy terms are used only for categorizing content on a Drupal powered web page, there should be no harm in deleting them. However sometimes taxonomy is used to store terms critical to the content they are referenced from and in this case steps should be taken to prevent an accidental deletion.

I have encountered such a case on a project I am working on which is soon to become a web platform for university students. When creating a faculty node, its name is being defined by choosing a term from the 'faculties' vocabulary. Deleting a term assigned to such a faculty node would lead to... well undesired effects.

Approach

When looking for the right hook you will find that there is no hook_taxonomy_pre_delete and using the existing hook_taxonomy_term_delete would be too late (the term would be deleted by then). (By the way, this problem persists across other entity types, like nodes - hoping to see some added hooks in D8.)

I will describe an easy way of preventing the deletion of a used taxonomy term, but be warned, this will only prevent the deletion of a term in the UI, it will not react to programmatically deleted terms.

Here is how this is going to look like:

Taxonomy term delete warning.

Some code:

  1. // In our custom module 'mymodule' let's go ahead and implement hook_form_alter() (what else!?) and switch on $form_id.

  2. function mymodule_form_alter(&$form, &$form_state, $form_id) {

  3. switch ($form_id) {

  4. // This is the general term form.

  5. case 'taxonomy_form_term':

  6. // Checking if we are on the delete confirmation form.

  7. if (isset($form['delete'])) {
  8. // Getting the term id.

  9. $tid = $form['#term']->tid;

  10. // Limiting the query to 30 to save resources when loading the nodes later on.

  11. $limit = 30;

  12. // Getting node ids referencing this term and setting a limit.

  13. $result = taxonomy_select_nodes($tid, FALSE, $limit);

  14. if (count($result) > 0) {
  15. $markup = t('This term is being used in nodes and cannot be deleted. Please remove this taxonomy term from the following nodes first:') . '
  16. // Looping through the node ids, loading nodes to get their names and urls.

  17. foreach($result as $nid) {

  18. $node = node_load($nid); // This is quite resource hungry, so if dealing with loads of nodes, make sure you apply a limit in taxonomy_select_nodes().

  19. if (!$node)

  20. continue;

  21. $markup .= '

  22. '</span> . l($node->title, 'node/' . $node->nid, array('attributes' => array('target'=>'_blank'))) . '
  23. '</span>;
  • }

  • // Appending some text with ellipsis at the end of list in case there might be more nodes referencing this term than the ones displayed.

  • if (count($result) >= $limit)
  • $markup .= '

  • '</span> . t("... only the first @limit results are displayed.", array('@limit' => $limit)) . '
  • ';

    About Drupal Sun

    Drupal Sun is an Evolving Web project. It allows you to:

    • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
    • Facet based on tags, author, or feed
    • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
    • View the entire article text inline, or in the context of the site where it was created

    See the blog post at Evolving Web

    Evolving Web