Upgrade Your Drupal Skills

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

See Advanced Courses NAH, I know Enough
Sep 08 2021
Sep 08

A Drupal e-commerce project

Drupal Commerce has been around for more than a decade. More than fifty thousand sites report using Commerce, and its maintainers have kept the suite of modules modern, flexible, and robust over the years for both Drupal 7 and Drupal 8/9 with the latest code for the Drupal 7 version released as recently as earlier this year.

The National Center for Employee Ownership (NCEO) came to us in 2018 with an existing investment in a Drupal 7 Commerce site that had thousands of hours poured into it in terms of configuration, custom development, and order management. At the time their staff were manually entering email and telephone orders into the Drupal Commerce backend, and were hoping to upgrade to a full-featured, payment gateway enabled online store. They also wanted to develop a brand new public facing NCEO.org that better captured their organization and connected users with their content.

At that time building a brand new public-facing website and upgrading an existing complex online store presented an interesting problem. Do we continue building on top of Drupal 7 knowing it will reach end-of-life in a couple of years? Or do we bite the bullet and invest in a new Drupal 8 solution with its promise of a future-proof approach to website development?

Spoiler alert — the answer was both.

Drupal 7 and Drupal 8: The best of both worlds

NCEO was using a custom Drupal 7 implementation of Commerce and RedHen CRM to manage nearly a thousand products, tens of thousands of customers, hundreds of thousands of order records, and complex coupon and membership pricing logic. Their existing platform represented an extensive investment — and their staff were already trained and proficient on the existing suite of tools. While the tides were definitely turning towards all things Drupal 8 in 2018, completely rebuilding the platform, migrating records, and training staff on a new system was a tough sell. And it was the wrong solution.

The right solution was to protect the existing investment, and building a lightweight Drupal 7 / Drupal 8 integration could do just that. Users needed to be able to browse NCEO’s content and products on a new Drupal 8 website, and then be seamlessly handed-off to a Drupal 7 Commerce backend to make payments or review order details. Luckily, Drupal’s architecture lends itself to just this sort of integration and synchronization across platforms.

A touch of tech

After extending NCEO’s Drupal 7 Commerce implementation to include an intuitive, turnkey customer checkout experience, all that was left to do was wire that backend to the brand new Drupal 8 NCEO.org that would roll out the following year.

The solution had to synchronize both users and products. The NCEO team would continue managing their products on the Drupal 7 platform, but those product updates would need to automatically post to the Drupal 8 site where users would browse them. Additionally, users would need to see their custom membership pricing on the Drupal 8 site, and maintain their session and membership data as they were seamlessly directed to the Drupal 7 platform for checkout.

Single sign-on & user data synchronization

We used the CAS module to create the “behind the scenes” single sign-on between the two sites, and to help synchronize the user role data that translates to special membership pricing and offers. When a user completes a membership purchase (on the Dupal 7 portion of the experience) their new user role is synchronized back to the Drupal 8 website — ensuring that they see the appropriate discounts and membership offers while browsing products, and later receive those discounts at checkout.

Product synchronization via the hook system

The NCEO team continues to manage products and customer records on the Drupal 7 portion of the platform. On the Drupal 7 side we built a simple webhook system that posts product update data using Drupal’s drupal_http_request($url, $options) everytime a product is created, updated, or deleted. We capture those actions with the insert, update, and delete node hooks available in the Drupal 7 API. In essence, every time someone makes a change to a product managed on the Drupal 7 platform, those changes are securely transmitted to the public facing Drupal 8 site.

When the data posts to the public-facing NCEO.org Drupal 8 site the products are updated almost immediately, ensuring that users are consistently presented with the absolute latest product information. Product updates on the Drupal 8 side are accomplished by defining a controller to accept data at a specific endpoint or URL — I wrote a pretty detailed post about capturing webhooks in Drupal 8 sometime ago, complete with code samples.

The integration is super lightweight and the end product is a seamless checkout experience. The solution lets the existing platform continue working as designed while rebuilding the primary public facing website with the more modern, evergreen Drupal 8.

Value before technology

As a developer there’s often an urge to design and build systems using the latest and greatest iterations of our favorite technologies. As the Director of Engineering at Aten, it’s important for me to consider how to achieve our clients’ goals in a way that’s cost effective, secure, and sustainable. Often that requires a mix of bleeding edge technology and creative out-of-the-box solutions that keep the people using the software at the center of the process.

In this case we were not only able to protect an existing investment while delivering a brand-new Drupal 8 website, but also built out a flexible architecture that will be super easy to upgrade in the future. In place of a Drupal 7 Commerce and RedHen CRM backend, NCEO could easily drop in another customer records solution to plug into their Drupal 8 site and store — all with hardly any changes to the Drupal 8 side. Even now in 2021 the solution works great — and as Drupal users everywhere decide what to do with their Drupal 7 assets, NCEO is in a great place to consider next steps for the commerce administration aspect of their platform without having to think about a complete website rebuild.

Interested in learning more about what innovative digital solutions can do for your organization? We’d love to hear from you.

Jun 22 2021
Jun 22

The code to programmatically copy field data in Drupal 8 is pretty simple, but I wasn’t able to find any great examples for performing the operation at scale when I ran into the need myself. Hopefully the code sample below helps somebody save a few minutes of digging for solutions.

My use case was straightforward: After more than a year of continual development on a client site, new patterns emerged in how content editors utilized certain entity reference fields. It became apparent that similar relationships across different content types used separate fields with different machine names. This made it tricky to aggregate and filter content. It also led to overly complex implementations of any custom functionality based on these relationships. One thing was clear: we needed the fields to be consistent across content types.

Basically, I needed entity references stored in various older fields, let’s say field_old_one and field_old_two, to be copied into a single destination field we’ll call field_new.

Once I’d added our field_new field to all the appropriate content types via the field UI, I needed to migrate the references out of the old fields, and into the new fields. What’s the best way to do this when manual GUI changes would take too long? A hook_post_update_NAME implementation was my answer.

The code to programmatically copy field data in Drupal 8 isn’t the hard part. Assuming your source and destination field types are compatible, it’s just two quick lines:

/* @var $node \Drupal\node\NodeInterface $node */
$node->$dest_field = $node->$source_field;
$node->save();

It gets complicated when you need to do this on hundreds or thousands of entities. We don’t want to load, process, and save all those entities at once as that would likely bring our server down. We need to batch the operation. Implementations of hook_post_update_NAME act as implementations of callback_batch_operation — allowing us to execute a custom callback _my_module_copy_field_values across multiple HTTP requests and avoid PHP timeouts. First, we define that callback.

/**
  * Copies the value from one field to another empty field.
  *
  * @param array $sandbox
  * The batch operation sandbox.
  * @param string $bundle
  * The node bundle.
  * @param string $source_field
  * The source field name.
  * @param string $dest_field
  * The destination field.
  * @param int $nodes_per_batch
  * The amount of nodes to update at a given time.
  */
function _my_module_copy_field_values(array &$sandbox, $bundle, $source_field, $dest_field, $nodes_per_batch = 20) {
  $storage = \Drupal::entityTypeManager()->getStorage('node');
  // Initialize some variables during the first pass through.
  if (!isset($sandbox['total'])) {
    $query = $storage->getQuery()
      ->condition('type', $bundle)
      ->notExists($dest_field)
      ->exists($source_field)
      ->accessCheck(FALSE);
 
    $nids = $query->execute();
 
    $sandbox['total'] = count($nids);
    $sandbox['ids'] = array_chunk($nids, $nodes_per_batch);
    $sandbox['current'] = 0;
  }
 
  if ($sandbox['total'] == 0) {
    $sandbox['#finished'] = 1;
    return;
  }
 
  $nids = array_shift($sandbox['ids']);
  $nodes = $storage->loadMultiple($nids);
 
  /* @var $node \Drupal\node\NodeInterface $node */
  foreach ($nodes as $node) {
    $node->$dest_field = $node->$source_field; // Programmatically copy field data
    $node->save();
    $sandbox['current']++;
  }
 
  $sandbox['#finished'] = min(($sandbox['current'] / $sandbox['total']), 1);
}

In this case, the $sandbox variable is passed by reference to each iteration of the post_update hook. It will loop over this function until $sandbox['#finished'] == 1 — or until the current node operation is equal to the total number of nodes.

In my case, I needed to run this operation on a few different fields across half a dozen content types each with their own mappings — I’ll just use article and post in my example below. To make it a little easier, I wrote the custom callback _my_module_copy_field_values to apply updates on a per content type and field basis. Then I called it from a few implementations of hook_post_update_NAME, one for each content type.

With your custom callback _my_module_copy_field_values defined, you simply call it in your hook_post_update_NAME implementations defined in your MY_MODULE.post_update.php file. Then when you run database updates, your work is done.

/**
 * Migrate Article field_old_one > field_new.
 */
function my_module_post_update_8001_migrate_article_field(&$sandbox) {
  _my_module_copy_field_values($sandbox, 'article', 'field_old_one', 'field_new');
}
/**
 * Migrate Post field_old_two > field_new.
 */
function my_module_post_update_8002_migrate_post_field(&$sandbox) {
  _my_module_copy_field_values($sandbox, 'post', 'field_old_two', 'field_new');
}

Executing your field level changes in hook_post_update_NAME versus hook_update ensures that any schema level changes (which should be in hook_update) are completed before your content level changes — just in case you’ve got some other updates going on or are working with a handful of other developers in the same codebase.

Do you have a simpler way to do this? Feel free to share in the comments.

Jun 15 2021
Jun 15

Drupal 7 is nearing end of life (EOL) 

Drupal 7 will reach end-of-life (EOL) in November of 2022, which means that at least a half million webmasters & site owners have some decisions to make. What’s the next step for your organization’s website? What sorts of costs might you be looking at for this upgrade? What timeline can you plan on for this change? All good questions.

What is EOL and what does it mean to me?

The Drupal ecosystem of core and contributed modules is protected against hackers, data miners, automated exploits and other malicious actors by the Drupal Security Team — more than thirty developers working across three continents in almost a dozen countries to keep Drupal websites safe. The security team responds to reports of potential weaknesses in the Drupal core or contributed code and coordinates efforts to release new versions of the software that address those vulnerabilities.

The more than a million Drupal developers worldwide going about their day-to-day development tasks act as a passive network of quality control agents. Developers who discover security vulnerabilities while working with the code can confidentially report them so that the security team can go about fixing the problem before knowledge of the vulnerability is widely available. A million worldwide developers backing a thirty-something strong team of elite developers spells security — for your website, your data, and your organization.

In November of 2022, that all comes to an end for Drupal 7. That’s when the security team will officially retire from the Drupal 7 project in favor of modern versions of the platform. As new vulnerabilities in the code are discovered (and made public) you won’t have anyone in your corner to fight back with new security releases.

After EOL you can expect what’s left of the Drupal 7 community to move on, too. That means no new modules, new themes, or other new features built for the platform. It also means the pool of developers specializing in Drupal 7 starts to shrink very fast. If you’re still on Drupal 7 in late 2022, you’re out in the cold.

The good news is that there are options. Here are my top three picks:

Drupal 9: Drupal is dead, long live Drupal

Drupal 9 is the most modern iteration of the Drupal framework and CMS. It introduces a completely reimagined architecture and a rebuilt API more inline with modern development standards.

  • Cost: High
  • Build Time: High
  • Longevity: High
  • Support: High

Pros

Upgrading your site to the latest version of Drupal (9.1.10 at the time of this article) is, in most cases, the right move. Modern Drupal has grown to support a wide array of innovative features in core. Improved WYSIWYG content editing, a feature rich Media library, advanced publishing workflows, and rich JSON API are all available right out of the box. Couple that with Drupal’s new, highly modern architecture built on Symfony, the adoption of the Twig templating engine, and dependency management via Composer and you really do Build the best of the web in terms of technology, support, and longevity.

When you make the move to Drupal 9 you can count on Drupal’s huge and thriving community of developers (and security team) making the move right along with you. Existing modules from previous versions of Drupal are either — in almost all cases — already available, now packaged into core, or making their way to Drupal 9 at this moment. It’s very likely the agency you’re already working with has or is building a Drupal 9 proficiency, and it’s guaranteed that hundreds of other shops can pick up the slack in the odd case that your provider isn’t on board yet.

Finally, let’s not forget Drupal’s commitment to easy future upgrades which promises continuity in architecture that should facilitate easy upgrades to Drupal 10 and beyond. Gone are the days (probably) of “rebuild” style upgrades like those of Drupal 6 to Drupal 7, or Drupal 7 to Drupal 8/9.

Cons

Speaking of “rebuild” style upgrades... upgrading from Drupal 7 to Drupal 9 is one of them. While it could be your last major upgrade if you stick with Drupal for the long haul, moving from Drupal 7 to the more modern 8/9 and beyond architecture is a very heavy lift. For most organizations the move to Drupal 9 is the longest term, most feature-rich, most supported, and most modern option, but it generally entails a complete rebuild of your site’s backend and theme which basically means starting from scratch. Take a look at Joel’s post about the upgrade from Drupal 7 to Drupal 8 for more information — the process is comparable to a Drupal 7 to Drupal 9 upgrade.

Backdrop CMS: The same, but different

Backdrop CMS is a lightweight, flexible, and modernized platform built on Drupal 7 architecture with notable improvements. The software is a fork of the Drupal 7 code and boasts a simple, straightforward upgrade path.

  • Cost: Low / Medium
  • Build Time: Low / Medium
  • Longevity: Medium / High
  • Support: Medium

Pros

Drupal 7 sites moving to any other platform — including Drupal 8 / 9 — must be rebuilt. That’s not the case for Backdrop CMS, which gives you the option of protecting your investment in an existing Drupal 7 website and reaping the benefits of modern features like configuration management and advanced layout control. Backdrop CMS will prioritize backward compatibility with Drupal 7 until at least 2024, meaning that even fully custom Drupal 7 code — with very minor modifications — should work in the Backdrop CMS ecosystem. A large selection of widely used Drupal 7 modules are already available for Backdrop CMS, and more are on the way. And while backwards compatibility is a major selling point for Backdrop CMS, its architecture is forward thinking with the introduction of classed entities and an object-oriented approach to all of its new components and features.

While the Backdrop CMS developer community isn’t particularly large, Drupal 7 and Backdrop CMS development skill sets are virtually interchangeable for the time being due to the almost identical API. There’s also considerable overlap on the Drupal 8/9 front due to Backdrop’s preference for object-oriented code in all of its newly added features. That means your existing Drupal developers can help you make the switch, and while the upgrade process isn’t exactly seamless it’s definitely a far cry from a complete rebuild.

Backdrop CMS also has its own security team, which — for now at least — works closely with the Drupal security team. Active development for the current version of Backdrop CMS is planned through 2024 according to their Roadmap, with the next version of Backdrop CMS promising an even easier upgrade path compared to the Drupal 7 to Backdrop CMS upgrade.

Cons

Backdrop CMS implements both Drupal 7 style procedural coding and Drupal 8/9 style object-oriented coding, which in theory means that it caters to a wide range of developers. In practice it’s hard to predict the future of any up-and-coming development community. That makes the outlook for long term support a little opaque, in that it’s hard to say just how many developers will be supporting Backdrop CMS and building new features for it down the road a couple of years.

Also, while Backdrop CMS absolutely prioritizes backwards compatibility with Drupal 7, a greater number of contributed and custom modules in your existing site could complicate the upgrade process. Simpler Drupal 7 sites with fewer contributed and custom modules would probably encounter a low effort to complete the upgrade, while a greater number of contributed and custom modules are likely to see a medium effort as some of those modules may need to be converted.

Drupal 7 Vender Extended Support: Don’t move a muscle

Drupal 7 ES is the do nothing for now option. A small collection of approved and vetted vendors will be providing security updates and / or critical patches for Drupal 7 core and contributed modules following a variety of vendor-specific plans.

Note: I added some material to this article after publication regarding the points identified with asterisks*, based on feedback from an industry peer. Check it out at the bottom of the post.

  • Cost: Low
  • Build Time: None*
  • Longevity: Low / Medium
  • Support: Medium

Pros

The biggest plus here is simple: No further action is required at this time.* If you’re planning to work with a vendor that provides extended support for Drupal 7, you won’t need to take any action to protect your website from aging software until Drupal 7 EOL in 2022. At that point, you’ll need to plan on a flat or adjustable monthly fee through the end of 2025 — or possibly beyond. This could mean avoiding major financial decisions regarding your digital strategy for at least a few years, and all at a cost that (depending on the size of your organization / website) is probably a fraction of the cost of a software upgrade.

With the Drupal 7 EOL recently extended until November of 2022, many of these Vender Extended Support plans haven’t fully materialized — so details are still forthcoming. Agencies like Tag1Quo or MyDropWizard advertise services from a surprising $15 / month to $1250 / month for a range of beyond EOL Drupal support plans. Acquia and Lullabot are also named by Drupal.org as ES vendors — but without any specifics about pricing or support levels. While the picture isn’t entirely clear yet, availability of an affordable ES plan is virtually guaranteed by 2022.

Cons

Drupal 7 Vendor Extended Support may protect you against vulnerabilities and exploits discovered after Drupal 7 EOL, but community support will be all but dead by that time. That means no new features or modules will be released and the pool of Drupal 7 developers will be rapidly drying up. Unless you have an in-house development team, you can plan on your website coming to a standstill in terms of new features.

The amount of contributed and custom modules your site implements has an impact here, too. The greater number of custom and contributed modules, the greater you can expect the effort to be in supporting those modules beyond EOL.

Another concern with Drupal 7 ES is PHP 7 end-of-life. Once PHP 7 (the language Drupal 7 is largely built on) is no longer supported towards the end of 2022, you can expect the security of your Drupal 7 site to rapidly degrade. Updating Drupal 7 to be compliant with the newer, more secure PHP 8 is doable — but could be a difficult process.

Finally, it’s likely that you will still need to consider upgrading to a supported platform in the future if your website will need to change and adapt in the coming years. You can expect this process to become more challenging as time goes on and the gap between your existing website and modern platforms grows ever larger.

Taking the first step

There are other options, too. Moving from Drupal 7 to another platform entirely (WordPress?) could make the most sense depending on the complexities of your website. Moving to a less robust CMS could be nominally more cost effective than an upgrade to Drupal 9, but it also bakes in some hard limitations to what your website will be able to do.

A lot of organizations are beginning to evaluate options for their Drupal 7 sites. The best way forward depends largely on your goals as an organization, your ambitions for your digital presence, and the amount of time and effort you’re willing to invest. We’d love to consider your questions or learn more about the specific challenges you’re facing as you sort through your options. Get in touch today with your questions about upgrade paths from Drupal 7.

Additional notes for Drupal 7 Vender Extended Support, added June 22, 2021:
"Build time: None" is not strictly accurate - users who choose Drupal 7 ES will probably need to install an update status module to inform their chosen vendor of version data for their core and contributed modules. They will also need to adopt a new update strategy as their module updates will be coming from a new source (their chosen vendor). These steps won't need to be taken until we're closer to Drupal 7 EOL - perhaps the Spring or Summer of 2022.

"No further action required at this time" may be misleading. Action will be required as we approach Drupal 7 EOL in the Spring or Summer of 2022 - namely choosing an ES vendor and following their instructions.

Jun 03 2021
Jun 03

Mapping Google Groups to Drupal user roles with Google Authentication seemed like a straightforward task, but there were a few wrinkles in the process that I didn’t expect. We stumbled into the need for mapping groups to roles while performing a routine Drupal core upgrade from 8.8.5 to 8.9.13. Complications with the existing LDAP authentication’s upgrade path — and conversations with the client — steered us towards replacing LDAP with Google Authentication.

Overall the idea is simple enough: when a user signs into your Drupal site using Google Authentication, their membership(s) to various Google Groups should map to some existing Drupal user roles. This way, users in your organization who are already affiliated with various groups can be automatically assigned appropriate user permissions in your Drupal site for editing or accessing content, approving comments, re-arranging homepage promotions or whatever the case may be.

The majority of the functionality is established with just a few downloads, installations, and configurations. Our custom functionality relies on the Social API, Social Auth, and Social Auth Google modules, as well as the Google APIs Client Library for PHP. The first step is to install and enable the above mentioned modules and library. Then comes a little insider info on configuring the necessary Google Service Account, and finally a custom implementation of the EventSubscriberInterface that reacts to authentication via Social Auth to read from Google Groups memberships. Our custom code is packaged into a simple demo module you can download to get all of this working for yourself.

There are a lot of moving parts for something you’d expect to be a simple task, but in the end it’s a relatively quick setup if you know what to expect.

The devil’s in the details: Google Service Account configuration

This bit is pretty counterintuitive: Google users that authenticate via Google Authentication don’t actually have read access to their own groups. Weird, right? Where are you supposed to pull group information if the accounts can’t read their own group data from the API?

The answer is a Google Service Account. A properly configured Google Service Account can have read access to both users and their group relationship information, which allows it to act as a go-between during authentication to pull Google Group data and map it to Drupal roles. Configuring your Google Service Account isn’t self-explanatory, but luckily Joel Steidl put together the following screencast to walk you through it. Once you’ve set up the Google Service Account, you’ll just need to create and download a JSON key file to include in the code we’ll walk through further below.

Now that your Google Service Account is properly configured and your JSON key file has been safely stored away, it’s time to tie everything together with a few blocks of code that do the actual role assigning.

Role assignment during Google Authentication: The who’s who handshake

Once you’ve configured your Google Service Account and downloaded your JSON key file, you need code that reacts to Social Auth events (like a login via Google Authentication) to trigger the role mapping functionality. I’ve added some example code below that's part of a demo module that should get you 95% of the way there, although in our demo the role mapping itself — which Google Group becomes which Drupal role — is hard coded.

If you download my demo module you’ll see I’m reacting to both the USER_CREATED and USER_LOGIN events provided by the Social Auth module’s getSubscribedEvents method. This way we can reevaluate Google Group to Drupal role mappings each time a user logs in to make sure their roles and permissions stay up to date. You’ll also see that I’m verifying the user’s email address to make sure it falls within our expectations ([email protected]_domain.com), which you can modify or remove depending on your needs — but you want to make sure that your Google Service Account will have the appropriate access to this user’s group info.

The magic happens in the determineRoles method, where the JSON key file and the Google Service Account you setup previously will come into play. You’ll need to modify the getSecretsFile method to return your JSON key file, then change the $user_to_impersonate variable to the email address that you granted the Groups Reader role as discussed in Joel’s screencast. Finally, you'll need to update the $roleAssignment array with actual Google Group names and Drupal roles.

/**
 * When a user logs in verify their Google assigned group & set permissions
 *
 * @param \Drupal\user\UserInterface $givenUser
 *   The passed in user object.
 *
 */
protected function determineRoles($givenUser)
{
  $KEY_FILE_LOCATION = $this->getSecretsFile();
 
  // Only run if we have the secrets file
  if ($KEY_FILE_LOCATION) {
    // 1. Admin SDK API must get enabled for relevant project in Dev Console.
    // 2. Service user must get created under relevant project and based on a user with
    // 3. User must have Groups Reader permission.
    // 4. Scope must get added to Sitewide Delegation.
    $user_to_impersonate = '[email protected]_domain.com';
    $client = new Google_Client();
    $client->setAuthConfig($KEY_FILE_LOCATION);
    $client->setApplicationName('Get a Users Groups');
    $client->setSubject($user_to_impersonate);
    $client->setScopes([Google_Service_Directory::ADMIN_DIRECTORY_GROUP_READONLY]);
    $groups = new Google_Service_Directory($client);
 
    $params = [
      'userKey' => $givenUser->getEmail(),
    ];
    $results = $groups->groups->listGroups($params);
 
    // Hold Map for roles based on Google Groups
    $roleAssignment = [
      "Author Group Name" => "author",
      "Editor Group Name" => "editor",
      "Publisher Group Name" => "publisher",
    ];
 
    // Loop through the user's groups an add approved roles to array
    foreach ($results['groups'] as $result) {
      $name = $result['name'];
 
      //Assign roles based on what was determined
      if (array_key_exists($name, $roleAssignment)) {
        $givenUser->addRole($roleAssignment[$name]);
        $givenUser->save();
      }
    }
  }
}

That’s it! With your modified demo module enabled, your users should get their roles automatically assigned on login. We were pretty excited to get Google Groups mapping to Drupal roles, so much so that my colleague Matthew Luzitano is considering packaging this functionality (and GUI configurations!) into a Drupal module. So if you’re not in any rush and don’t feel like browsing our demo code, you can always wait until that happens.

Do you have a different approach to mapping Google Groups to Drupal roles? We’d love to hear about it! Let us know in the comments below.

Apr 23 2021
Apr 23

Aten loves libraries. We’ve built a range of software solutions for libraries all over the country, including the John D. Rockefeller Jr. Library in Williamsburg, VA, the Denver Public Library in our own Denver county, the Richland Library in South Carolina, Nashville Public Library in Tennessee and the Reaching Across Illinois Library System (“RAILS”). It’s remarkable just how many commonalities libraries share when it comes to the features, tools, and considerations their websites need to better serve their users.

Some of those similarities are a no-contest justification for building common, configurable library solutions — solutions like Intercept: a reimagined, generalized approach to library event management, room and equipment reservation, and customer tracking. We co-developed Intercept for Drupal with Richland Library, reused it with L2 / RAILS and actively maintain it in the hopes that it goes on serving libraries for years to come. But for all of the similarities we see between libraries and their digital needs, there are some key differences, too.

Complex permissions needs

Library Directory and Learning Calendar (“L2”) — a project of RAILS — had unique permissions needs. Their staff needed custom view, edit, and delete permissions to website content managed at different organizational levels of the library system. Those organizational levels, structured hierarchically from Illinois State Library, to Regional Library System, through Catalog Consortium, Agency and finally Location, needed associated cascading permissions — i.e., permissions granted at a higher organizational level should cascade to associated content in lower organizational levels. An administrator for an Illinois State Library piece of content, for example, should be able to exercise their administrative permissions on Regional, Consortium, Agency, and Location content associated with that State Library content. An administrator for an Agency would have administrative permissions for that Agency’s Locations.

Granular role based Drupal permissions wouldn’t cut it. That’s because with standard Drupal permissions, each user role can be assigned view, edit, and delete permissions to each content type (say any Regional Library System Page), but we needed to assign those permissions to the appropriate instances of a content type — like the specific Regional Library System Page that belongs to the west-central region, for example.

There are plenty of contributed modules that start down the right path towards (for lack of a better term) cascading permissions by content affiliation, but they wouldn’t have gotten us all the way there. Both the Group and Permissions by term modules, for example, can be incredibly useful in similar situations. In this case, given the features and functionality contributed modules would introduce that we don’t need, plus the level of modification necessary to achieve our goals, we decided on a lightweight, custom solution.

Role and affiliation based custom permissions for L2

Permissions for L2 staff are established using a custom affiliation entity, which stores data about the role a particular user has in relation to a specific piece of content. The custom affiliation entity references a user, a role (a taxonomy term like Admin, Manager, or Staff, for example), and a specific piece of content (a node). A variety of other fields are established in the same affiliation entity in order to store additional metadata about the relationship like contact information, job title, job description, or other details.

Custom affiliation entity field configuration screen, Drupal 8 Affiliation, Role / Access Group, and User fields establish a permission relationship. Metadata fields (blurred here) can provide some arbitrary data about the relationship.

The custom affiliation entities are organized into their own bundles, one for each of the hierarchically structured organizational levels previously described: Illinois State Library, Regional Library System, Catalog Consortium, Agency, and finally Location. This way each individual type of affiliation can contain the metadata fields appropriate for its specific organizational level. Finally, there is an arbitrary Group affiliation which affiliates a user with a piece of content without granting the cascading permissions that accompany standard affiliations.

Custom entity affiliation entity types, Drupal 8 Each organizational level is represented by its own custom affiliation entity type.

The content items (read nodes) whose permissions we’re controlling are organized along the same organizational levels: Illinois State Library, Regional Library System, etc. Each of these content types uses a unique entity reference field to establish a parent / child relationship along the organizational levels. Locations are associated with Agencies, Catalog Consortia, Regional Library Systems or directly with the Illinois State Library; Agencies are associated with other Agencies and / or Regional Library Systems; Catalog Consortia are associated with Regional Library Systems; and Regional Library Systems with the single parent Illinois State Library. It’s a complicated web!

Cascading permissions diagram Permissions granted on content at any one organizational level cascade to associated content in the lower levels.

Once the content for L2 was developed and properly structured with the appropriate parent / child relationships, granting a user specific view, edit, and delete permissions for a particular region of the structured content tree was simple. Simply create an affiliation that assigns the user a role in relation to content at a specific level of the organization, and voila! — the user gains access to that content and all of its children at each of the lower levels.

The permission grid: Tying it all together

One last element ties our whole permissions system together: a robust permissions map that associates view, edit, and delete permissions per custom defined Role / Access Group in relation to various entities. Unlike the unwieldy Drupal permissions grid that assigns roles to broadly defined permissions with the help of about a million radio buttons, our definitions can be static (think code, not GUI) and only have to deal with view, edit, and delete permissions for entities or entity fields. Each Role / Access Group has its view, edit and delete permissions defined per bundle or per specific bundle / field combination, resulting in very cleancut — and extremely granular — permission control.

[
  {
    "entity": "node",                         // Entity type we're granting access to
    "field": "",                              // Field for this entity, left blank we're defining with entity level access
    "affiliation bundle": "location",         // Affiliation entity that controls access, in this case the location affiliation type
    "target bundle": "location",              // Entity bundle we're granting access to, in this case a location node
    "role_access_group": "location_manager",  // Custom defined Role / Access Group that grants this permission
    "edit": 1,                                // Edit permission
    "view": "",                               // View permission
    "delete": ""                              // Delete permission
  }
]

Our “permissions grid” is made up of about 500 similar declarations in a single JSON file, which range through a variety of Roles / Access Groups, a couple of entity types, and tons of bundle / field combinations for some of the more complex field level permissions.

Individual permissions grants are then handled through hook_ENTITY_TYPE_access() and hook_entity_field_access(), which use a a custom Service to load all of the requesting user’s affiliation entities, determine their role in relation to the content (node) in question, then find that role’s particular permissions using our custom JSON permissions map. Here’s an example for node access.

/**
 * Determines if the operation is allowed for a node.
 *
 * @param object $relationship
 *   The relationship object.
 * @param string $operation
 *   The operation being attempted.
 * @param Drupal\node\Entity\Node $node
 *   The node on which the operation is being attempted.
 *
 * @return bool
 *   True if the operation is allowed.
 */
public function nodePermission(object $relationship, $operation, Node $node) {
  // Create an array of data from l2_access_permissions.json, each element
  // of which is an array with elements matching $relationship object
  // properties.
  $module_path = drupal_get_path('module', 'l2_access');
  $matrix = json_decode(file_get_contents($module_path . '/l2_access_permissions.json'),
    TRUE);
 
  // Create an array matching the structure of $matrix elements to see if
  // it matches any $matrix elements (which would mean that there might be
  // a permission that allows this $operation.)
  $relationship_array = [
    'entity' => 'node',
    'field' => '',
    'affiliation bundle' => $relationship->affiliation_bundle,
    'target bundle' => $relationship->target_bundle,
    'role_access_group' => $relationship->role_access_group,
  ];
 
  // Set the $relationship_array's 'edit' and 'view' elements based on the
  // $operation's value.
  $operation = ($operation == 'update') ? 'edit' : $operation;
  $operations = ['view', 'edit', 'delete'];
  foreach ($operations as $op) {
    $relationship_array[$op] = ($op == $operation) ? 1 : "";
  }
 
  // Handy here that array_search() can test whether an array is an element
  // in another array. Here: is $relationship_array an element of $matrix?
  $match = array_search($relationship_array, $matrix);
  if (!$match) {
    return FALSE;
  }
 
  // Found a match. Does it allow access?
  switch ($operation) {
    case 'edit':
      if ($matrix[$match]['edit'] == 1) {
        return TRUE;
      }
      break;
 
    case 'update':
      if ($matrix[$match]['edit'] == 1) {
        return TRUE;
      }
      break;
 
    case 'view':
      if ($matrix[$match]['view'] == 1) {
        return TRUE;
      }
      break;
  }
 
  // If we're here, this $relationship doesn't provide $operation access.
  return FALSE;
}

The end result is powerful and flexible. Our JSON permissions map tells us which Roles / Access Groups have which permissions per entity or entity / field combination. The custom affiliation entities grant users a specific Role / Access Group in relation to a specific piece of content, and that content’s entity references to other entities allow the permissions to cascade to entities in lower organizational levels.

That may sound like a lot, but it’s surprisingly simple. The solution boils down to a handful of entity access hook implementations that use a custom service to lookup the current user’s permissions by affiliation via a JSON permissions map. The total footprint sits at around 1000 lines of code — not counting the permissions map itself — and flexibly manages thousands of users’ permissions across thousands of complex, hierarchical content relationships down to the field level. Pretty neat.

Apr 15 2021
Apr 15

Content authoring on the web has evolved. Compelling web content uses a variety of multimedia elements to engage users, tell stories, build brands, and share new ideas. Images, video clips, slideshows, static or parallax backgrounds, block quotes and text pullouts — more than ever, content creators need a tool that embraces an evolving medium and keeps pace with the author’s creativity. We believe Layout Paragraphs is that tool. But first, let’s all agree that the body field is dead.

What is the body field

Web editors and content authors who have been around over the last decade of digital media are intimately familiar with the body field. Many popular content management systems (looking at you, Drupal and Wordpress) feature a standard “post” or “content” type — like page, article, or blog post — right out of the box. These content types often have a couple of fields to fill out before you can publish your post, things like title, tags, friendly URL / slug, and of course body.

The body field, traditionally, is where all your content goes. In the early days of blogging and web publishing that was likely to be all or mostly text, but with time came images, videos, and eventually an array of other multimedia elements. And the body field adapted. Tokens became standard for plenty of web editors — cryptic chunks of text like [[nid:376 align:right]] that would be auto-magically replaced with other elements once you click Publish. WYSIWYG editors (What You See Is What You Get) started shipping with Insert image and Insert video buttons, and began including a variety of tools for encapsulating, aligning, and positioning lengths of text or various media elements. And while these accommodations started to connect content creators with the boundless possibilities of multimedia authoring, they were (and are) clumsy and unpredictable.

Structured content: Reduce, reuse, recycle

One major problem with all of these innovations is that once you click Publish, all that complex content still ends up in the body field — that is, saved into your database as one giant clump of complicated text, tokens, style codes, etc. If you’d like to publish another page with a similar look & feel, get ready to re-inject all of your tokens or go through the same WYSIWYG click-a-thon to reestablish your styles and layout. Thinking of producing a list of all the images used in your posts? Good luck! With all content mushed together into a single field, your site doesn’t “know” the difference between a paragraph of text, an image, or a video.

A structured approach to content organizes each individual element — a pane of text, an image, slideshow, video, or a “donate now” banner — as its own self-contained entity which, ideally, could then be placed within flexibly configured regions on a page. Creating other, similar pages then becomes as simple as swapping out individual elements or shifting them around between regions of a page. And libraries of reusable content elements become the norm — so that you’re now picking your images, videos, donate banners and more from lists of existing content or existing content styles, instead of trying to hunt down how you did something similar the last time.

Structuring your content into individual, reusable elements drives a lot of collateral benefits:

  • Ease of content creation. Authoring with a consistent set of components lets you focus more on content, hierarchy, and intent — instead of trying to remember how you got a video in there at all the last time.
  • Styling and restyling. Using discrete elements of content means that your HTML markup and CSS remains consistent across all of your various pages. When the time comes to update the look & feel of your website, changes made on one article page will “just work” on the rest of your articles and all of their elements.
  • Content migration. The day might arrive when you consider migrating your content to a newer platform or different software. Structured content makes that a snap: Each image, video, slideshow, or paragraph of text gets individually ported to its new home on the new platform. On the contrary, migrating a mess of markup, tokens, and style codes stored in a single body field means writing complex, custom code to recognize those items and deal with them appropriately — not a simple task.
  • Syndication. Want to feature your content in a collection via RSS or deploy it to an app with XML? With structured content apps and other websites can consume just the elements they want of your content (maybe the first paragraph, a title, an image, or a link) then display those elements appropriately according to their own styles — rather than just grabbing a few hundred lines of a single, messy body field and making do.
  • Libraries, lists, and cross-promotion. Want to see all the slideshows that appear in articles with a specific tag? Maybe a list of pull-quotes from your most recent blog posts? Structured content creates a world of opportunity around libraries, lists and cross-promotions.

Structuring content facilitates reuse, and it positions your content to go on living in a variety of platforms or a variety of presentations — long after you first click the Publish button.

The two faces of web content

Structuring content is an essential part of beautiful, intuitive digital authoring, but it’s only one side of the coin. The other side is putting an end to the two faces of your web content. Excepting some of the advances of modern WYSIWYG editors, web content has always had two very different faces: View and Edit.

The way content is edited and the way it’s presented are often strikingly different. Especially if you’re still holding on to “one giant body field” innovations like custom tokens, BBCode, Markdown, or short codes, it can take a handful of Preview to Edit to Preview roundtrips to get your content exactly how you want it. Even if you’ve made the move to structured content, a majority of web-based editors are marked by a dramatic difference between the “back end” and the “front end” — just one more digital convention getting between inspired authors and publishing beautiful content easily.

The king is dead, long live the king

The body field is dead. It’s taken center stage in publishing digital content for long enough. Plenty of web products (Medium and Notion, to name a couple) have driven nails in that coffin for publishers creating content on those specific platforms. But what about content creators and web editors working within their organizations’ websites, in custom web applications where content authoring is just one of several important features? What about you, and your organization’s website? What’s next for your web application?

For Drupal websites, we think Layout Paragraphs is what’s next. It was first released to the Drupal community nearly a year ago, and has been the beneficiary of ambitious development ever since.

The Layout Paragraphs module makes it dead simple for authors to create robust, multimedia content from a library of available components. It offers intuitive drag-and-drop controls for managing content flow and layout, and with appropriate styling can bring the two faces of web content — View and Edit — closer than ever before. We built Layout Paragraphs to embrace the future of multimedia content authoring and to solve the problems we watch clients work through every day.

You can watch a short, two-minute demo of Layout Paragraphs here, or follow the instructions in that post to see it in action for yourself.

Mar 11 2021
Mar 11

Upgrading from Drupal 7 to Drupal 8 is a major project (a full rebuild in most cases) that can rival or even top the original cost of application development. Upgrading from Drupal 8 to 9 and beyond, in comparison, requires just a tiny fraction of that effort. Here’s a look at why — and why you should take the leap from Drupal 7 to Drupal 8 and 9.

The upgrade from Drupal 7 to Drupal 8 was a major theme for plenty of our clients over the last few years. Drupal 8 — and versions beyond — boast a wide range of benefits that stem from a more modern architecture, but one of the biggest wins in my mind is the advent of semantic versioning (for Drupal) and a renewed commitment to making Drupal upgrades easy forever. Moving to Drupal 8 could be your last major upgrade.

The ghost of Drupal past

The move from Drupal 7 to Drupal 8 isn’t easy. While it’s often still touted as an “upgrade process” the reality is that it boils down to a complete rewrite of the codebase and a content migration. We’ve worked on more than a handful of Drupal 7 to Drupal 8 upgrades in the last years, and the majority constitute a “six months or more” sized project.

One reason for that is Drupal’s new Symfony reliant architecture that leans far closer to the tenets of object oriented programming (OOP) versus the more procedural approach used in previous Drupal versions — a significant technical change which in most cases means Drupal 7 code simply won’t work in Drupal 8. Add to that a completely revamped templating system and you get a similar obstacle on the front-end: Drupal 7 templates & templating systems won’t work in Drupal 8. Those major changes along with a potentially long list of complications with Drupal 8 data migrations is likely to land your Drupal 7 to Drupal 8 upgrade squarely in “rebuild the application” territory.

The good news is that it’s the last time.

Drupal 8 and beyond: Your last major upgrade

Drupal 8, 9 and beyond promise a continued investment in modern architecture, incremental major feature releases, greater stability and sustainability, and a much larger network of invested developers — among lots of other perks. Perhaps the best news to come along with the new Drupal architecture is that large-scale, overhaul-style major version upgrades will be a thing of the past. And we’re beginning to see that now as we begin moving Drupal 8 sites to Drupal 9.

Clearing the road ahead: A commitment to easy upgrade paths

With Drupal 8 comes the commitment to semantic versioning. Semantic versioning isn’t just a naming convention for software versions, it’s a descriptive norm that guides how new versions of code should be written and establishes strict backwards compatibility requirements. The upshot for end users is significant but manageable incremental changes between major versions (say 8.x and 9.x) which aim towards seamless major version upgrades. Major versions can no longer make dramatic jumps (like Drupal 8’s move to OOP or the Twig templating system) which necessitate major code rewrites, but instead make small, reasonable changes between minor versions.

With the new approach to versioning developers get a variety of automated tools that ease the shift between minor and major versions. Deprecation checking means developers get notices in their code editors when parts of the existing codebase have been marked as deprecated, i.e., queued for change or removal in an upcoming major version. Automated tools like Upgrade Status or Drupal Rector and its Drupal front-end Upgrade Rector can provide detailed upgrade status information and even automatically update code between major versions. All in all, upgrade paths are looking easier than ever.

A wider foundation: Sustainability through community (and Symfony)

Building Drupal on top of Symfony means even further specialization on the Drupal end. The new Drupal versions use Symfony for a host of standard web application tasks — things like form validation, data serialization, data storage and retrieval, content translation, templating, easy and human-readable configuration management with YAML — the list goes on. Building on top of Symfony lets Drupal developers build closer to the consumer level, i.e., build the media libraries, authoring experiences, layout managers, etc that really matter to users.

Drupal’s reliance on Symfony — a robust and mature web application framework with more than 600,000 registered developers — also translates to greater sustainability through a larger active community. Besides adding Symfony’s 600K developers to Drupal’s roughly 1 million, the move to a modern architecture and more collaborative versioning system makes Drupal more attractive to developers on the leading edge of their disciplines. That fact alone stands to benefit the Drupal community through new, top-tier talent driving further innovation.

Regular new stuff: Incremental feature releases

With Drupal’s new versioning cycle comes another benefit: major new features in minor version core releases. As Drupal 8 churns through minor releases (the second place in the three place version number, e.g. “y” in x.y.z) it continues to add brand new features to core. Unlike previous major Drupal versions, these aren’t just bug fixes and security upgrades. Game changing modules like JSON API, Media, and Layout Builder were all added to Drupal 8 in minor version releases — and more are in the works.

What does this mean for end users? A greater commitment to standardized features that meet primary needs (like media and layout management, for example) and a bigger pool of developers making sure those features are updated, stable, and compatible.

I just finished one of my first Drupal 8/9 upgrades yesterday for a small searchable database of computer science tips & tricks run by a college out of Claremont, California. Admittedly it was a pretty simple site, but the upgrade took me just two hours. Wow.

Aten will be working on a variety of Drupal 8 to Drupal 9 upgrades in the coming months, but the landscape is almost unrecognizable compared to our Drupal 7 to Drupal 8 efforts. For us, for the rest of the Drupal world, and for our clients, that is real good news.

Nov 18 2020
Nov 18

The Authoring Experience Problem

Authoring experience is a huge concern for web teams everywhere. As it should be. The ability for digital publishers to keep up with the rapidly accelerating demands of their users, and the pace of their competitors, largely depends on providing easy-to-use, expressive tools for their teams to create compelling content.

In practice, this means that publishers need the ability to quickly produce rich digital content from a range of possible components: text, images, videos, social media assets, and so on. Authors should be able to mix various elements quickly and effortlessly, with simple controls for managing content flow and layout. Under the hood, content should be structured and predictable. We’ve written about this before: organizations cannot sacrifice structure for flexibility. They need both.

And yet authoring experience has been a major shortcoming of Drupal, often articulated by marketing and editorial leaders as, “Drupal is too complicated for my organization.”

Other platforms from across the entire spectrum of digital publishing systems – from Adobe Experience Manager to Wordpress, Squarespace to WiX – provide rich, intuitive tools for easily creating all kinds of digital content. What Drupal offers in terms of flexibility – powerful APIs, easily customizable structured content, and a huge range of content management features – has come at the expense of the authoring experience.

Layout Paragraphs

A while ago I wrote about Entity Reference with Layout (or ERL), a Drupal 8 module that combines Paragraphs with the Layout API for a more flexible, expressive authoring experience in Drupal. ERL had one concerning drawback: you had to use a special field type for referencing paragraphs. That means ERL wouldn’t “just work” with existing paragraph fields.

Layout Paragraphs, the successor to ERL, fixes that problem. The Layout Paragraphs module combines Drupal Paragraphs with the Layout API and works seamlessly with existing paragraph reference fields.

The Layout Paragraphs module makes it dead simple for authors to build rich pages from a library of available components. It offers drag-and-drop controls for managing content flow and layout, and works seamlessly with existing paragraph reference fields.

Layout Paragraphs features include:

  • Easily configure which content components, or paragraph types, are available to your authors wherever Layout Paragraphs is used.
  • A highly visual, drag-and-drop interface makes it simple to manage content flow and layout.
  • Site admins can easily configure Layout Paragraphs to support nested layouts up to 10 levels deep.
  • A “disabled bin” makes it easy to temporarily remove content elements without deleting them.
  • Layout Paragraphs works seamlessly with existing paragraph reference fields.
  • Authors can easily switch between different layouts without losing nested content.
  • Layout Paragraphs is fully customizable and works with Layout Plugins, Paragraph Behaviors, and other Drupal APIs.

What About Layout Builder?

While there are strong similarities between Layout Paragraphs and Layout Builder, Layout Paragraphs solves a fundamentally different problem than Layout Builder.

Layout Builder, in Drupal core, is a powerful system for managing layouts across an entire website. With Layout Builder, site administrators and content managers can place content from virtually anywhere (including custom blocks) within specific regions of a layout. Layout Builder is extremely powerful, but doesn’t directly solve the problem I mentioned above: “Drupal is too complicated for my organization.”

Layout Paragraphs adds a new interface for effortlessly managing content using Drupal Paragraphs. It is simple, fast, and expressive. Layout Paragraphs is a field widget, and works the same way as other Drupal fields. The Layout Paragraphs module makes it simple for publishers to create and manage rich content from a library of available components: text, images, videos, etc. Layout Paragraphs is completely focused on the authoring experience.

Try It Out

If you want to see Layout Paragraphs in action, simply download the module and give it a try. Setup is fast and easy. From the Layout Paragraphs module page:

  1. Make sure the Paragraphs module is installed.
  2. Download/Require (composer require drupal/layout_paragraphs) and install Layout Paragraphs.
  3. Create a new paragraph type (admin > structure > paragraph types) to use for layout sections. Your new paragraph type can have whatever fields you wish, although no fields are required for the module to work.
  4. Enable the “Layout Paragraphs” paragraph behavior for your layout section paragraph type, and select one or more layouts you wish to make available.
  5. Make sure your new layout section paragraph type is selected under “Reference Type” on the content type’s reference field edit screen by clicking “edit” for the respective field on the “Manage fields” tab.
  6. Choose “Layout Paragraphs” as the field widget type for the desired paragraph reference field under “Manage form display”.
  7. Choose “Layout Paragraphs” as the field formatter for the desired paragraph reference field under “Manage display”.
  8. That’s it. Start creating (or editing) content to see the module in action.

If you’re using Layout Paragraphs in your projects, we’d love to hear about it. Drop a note in the comments section below, or get involved in the issue queue.

Nov 12 2020
Nov 12

Creating absolute menu links in Drupal 8 is pretty simple, but it took a little thought to land on the best solution for my use case. I ran into the requirement while working with a client to deploy their site navigation to a REACT app sitting on a different domain. The menu links we pulled in from their primary website needed to point back to their domain correctly, which of course meant they needed to be absolute.

A quick search turned up a couple of solutions, but none that quite suited my requirements. Here’s what I came up with — it’s fairly concise, leaves the menu link attributes intact, and doesn’t involve the theme layer.

/**
 * Implements hook_preprocess_menu().
 *
 */
function MYTHEME_preprocess_menu(&$variables) {
  // Limit to specific menus for performance.
  $menus = ['main', 'footer'];
  if (isset($variables['menu_name']) && in_array($variables['menu_name'], $menus)) {
    MYTHEME_set_menu_items_to_absolute($variables['items']);
  }
}
 
/**
 * Recursively make menu items link with absolute.
 *
 */
function MYTHEME_set_menu_items_to_absolute(&$items) {
  foreach ($items as $item) {
    $url = $item['url'];
    if (!$url->isExternal()) {
      $item['url'] = $url->setOption('absolute', TRUE);
    }
    // Recursively loop of sub-menu items.
    if (isset($item['below'])) {
      MYTHEME_set_menu_items_to_absolute($item['below']);
    }
  }
}

Generating absolute menu links this way means I can pull main navigation onto various apps without any headache and without sacrificing any Drupal theme layer functionality.

You can download the gist here. Make sure you specify which menus you'd like to act on, by machine name, in the first line of the MYTHEME_preprocess_menu() function. The code goes in mytheme.theme — don't forget to replace MYTHEME with your theme name throughout.

Do you have a simpler way? Feel free to share in the comments.

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