Jan 31 2019
Jan 31

We do a lot of Drupal 8 migrations here at Aten. From older versions of Drupal and Wordpress, to custom SQL Server databases, to XML and JSON export files: it feels like we’ve imported content from just about every data source imaginable. Fortunately for us, the migration system in Drupal 8 is extremely powerful. It’s also complicated. Here’s a quick-start guide for getting started with your next migration to Drupal 8.

First, a caveat: we rarely perform simple one-to-one upgrades of existing websites. If that’s all you need, skip this article and check out this handbook on Drupal.org instead: Upgrading from Drupal 6 or 7 to Drupal 8.

It’s Worth the Steep Learning Curve

Depending on what you’re trying to do, using the migrate system might seem more difficult than necessary. You might be considering feeds, or writing something custom. My advice is virtually always the same: learn the migrate system and use it anyway. Whether you’re importing hundreds of thousands of nodes and dozens of content types or just pulling in a collection of blog posts, migrate provides powerful features that will save you a bunch of time in the long run. Often in the short run, for that matter.

Use the Drupal.org Migrate API Handbooks

There’s a ton of great information on Drupal.org in the Migrate API Handbooks. Be prepared to reference them often – especially the source, process, and destination plugin handbooks.

Basic Steps

Here’s a much simplified overview of the high-level steps you’ll use to set up your custom Drupal 8 migration:

All Migrations

  • Enable the migrate module (duh).
  • Install Migrate Tools to enable Drush migration commands.
  • Install Migrate Plus as well. It provides a bunch of extensions, examples and plugins for migrations. I’d just assume you need it.
  • Create a custom module for your migration.
  • Use YAML configuration files to map fields from the appropriate source, specifying process plugins for necessary transformations, to the destination. The configuration files should exist in “my_migration_module/config/install/“.
    (Pro tip: you’ll probably do a lot of uninstalling and reinstalling your module to update the configuration as you build out your migrations. Use “enforced dependencies” so your YAML configurations are automatically removed from the system when your module is uninstalling, allowing them to be recreated – without conflicts – when you re-enable the module.)

Enforced dependencies in your YAML file will looks something like this:

dependencies:
  enforced:
    module:
      - my_migration_module

See this issue on Drupal.org for more details on enforced dependencies, or refer to the Configuration Management Handbooks.

Drupal-to-Drupal Migrations

  • If you’re running a Drupal-to-Drupal migration, run the “migrate-upgrade” Drush command with the “--configure-only” flag to generate stub YAML configurations. Refer to this handbook for details: Upgrade Using Drush.
  • Copy the generated YAML files for each desired migration into your custom module’s config/install directory, renaming them appropriately and editing as necessary. As stated above, add enforced dependencies to your YAML files to make sure they are removed if your module is uninstalled.

Process Plugins

Process plugins are responsible for transforming source data into the appropriate format for destination fields. From correctly parsing images from text blobs, to importing content behind HTTP authentication, to merging sources into a single value, to all kinds of other transformations: process plugins are incredibly powerful. Further, you can chain process plugins together, making endless possibilities for manipulating data during migration. Process plugins are one of the most important elements of Drupal 8 migrations.

Here are a few process plugin resources:

Continuously Migrate Directly from a Pantheon-Hosted Database

Most of our projects are hosted on Pantheon. Storing credentials for the source production database (for example, a D7 website) in our destination website (D8) code base – in settings.php or any other file – is not secure. Don’t do that. Usually, the preferred alternative is to manually download a copy of the production database and then migrate from that. There are plenty of times, though, where we want to perform continuous, automated migrations from a production source database. Often, complex migrations require weeks or months to complete. Running daily, incremental migrations is really valuable. For those cases, use the Terminus secrets plugin to safely store source database credentials. Here’s a great how-to from Pantheon: Running Drupal 8 Data Migrations on Pantheon Through Drush.

A Few More Things I Wish I’d Known

Here are a few more things I wish I had known about back when I first started helping clients migrate to Drupal 8:

Text with inline images can be migrated without manually copying image directories.

It’s very common to migrate from sources that have inline images. I found a really handy process plugin that helped with this. In my case, I needed to first do a string replace to make image paths absolute. Once that was done, I ran it through the inline_images plugin. This plugin will copy the images over during the migration.

body/value:
   -
     plugin: str_replace
     source: article_text
     search: /assets/images/
     replace: 'https://www.example.com/assets/images/'
   -
     plugin: inline_images
     base: 'public://inline-images'

Process plugins can be chained.

Process plugins can be chained together to accomplish some pretty crazy stuff. Sometimes I felt like I was programming in YAML. This example shows how to create taxonomy terms on the fly. Static_map allows you to map old values to new. In this case, if it doesn’t match, it gets a null value and is skipped. Finally, the entity_generate plugin creates the new taxonomy term.

 field_webinar_track:
   -
     plugin: static_map
     source: webinar_track
     map:
       old_tag_1: 'New Tag One'
       old_tag_2: 'New Tag One'
     default_value: null
   -
     plugin: skip_on_empty
     method: process
   -
     plugin: entity_generate
     bundle_key: vid
     bundle: webinar_track

Dates can be migrated without losing your mind.

Dates can be challenging. Drupal core has the format_date plugin that allows specifying the format you are migrating from and to. You can even optionally specify the to and from time zones. In this example, we were migrating to a date range field. Date range is a single field with two values representing the start and end time. As you can see below, we target the individual values by specifying the individual value targets as ‘/’ delimited paths.

 field_date/value:
   plugin: format_date
   from_timezone: America/Los_Angeles
   from_format: 'Y-m-d H:i:s'
   to_format: 'Y-m-d\TH:i:s'
   source: start_date
 field_date/end_value:
   plugin: format_date
   from_timezone: America/Los_Angeles
   from_format: 'Y-m-d H:i:s'
   to_format: 'Y-m-d\TH:i:s'
   source: end_date

Files behind http auth can be copied too.

One migration required copying PDF files as the migration ran. The download plugin allows passing in Guzzle options for handling things like basic auth. This allowed the files to be copied from an http authenticated directory without the need to have the files on the local file system first.

   plugin: download
   source:
     - '@_remote_filename'
    - '@_destination_filename'
   file_exists: replace
   guzzle_options:
     auth:
       - username
       - password

Constants & temporary fields can keep things organized.

Constants are essentially variables you can use elsewhere in your YAML file. In this example, base_path and file_destination needed to be defined. Temporary fields were also used to create the exact paths needed to get the correct remote filename and destination filename. My examples use an underscore to prefix the temporary field, but that isn’t required.

source:
 plugin: your_plugin
 constants:
   base_path: 'https://www.somedomain.com/members/pdf/'
   file_destination: 'private://newsletters/'
 
 _remote_filename:
   plugin: concat
   source:
     - constants/base_path
     - filename
 _destination_filename:
   plugin: concat
   source:
     - constants/file_destination
     - filename
 
   plugin: download
   source:
     - '@_remote_filename'
     - '@_destination_filename'
   file_exists: replace
   guzzle_options:
     auth:
       - username
       - password

This list of tips and tricks on Drupal Migrate just scratches the surface of what’s capable. Drupalize.me has some good free and paid content on the subject. Also, check out the Migrate API overview on drupal.org.

Further Reading

Like I said earlier, we spend a lot of time on migrations. Here are a few more articles from the Aten blog about various aspects of running Drupal 8 migrations. Happy reading!

Sep 27 2018
Sep 27

As of Fall 2018 and the release of Drupal 8.6, the Migrate module in core is finally stabilizing! Hopefully Migrate documentation will continue to solidify, but there are plenty of gaps to fill.

Recently, I ran into an issue migrating Paragraph Entities (Entity Reference Revisions) that had a few open core bugs and ended up being really simple to solve within prepareRow in the source plugin.

Setup

In my destination D8 site, I had a content type with a Paragraph reference field. Each node contained one or more Paragraph entities. This was reflected by having a Node migration with a dependency on a Paragraph Entity migration.

With single value entity references, the migration_lookup plugin makes it really easy lookup up entity reference identifiers that were previously imported. As of September 2018, there is an open core issue to allow multiple values with migration_lookup. Migration lookup uses the migration map table created in the database to connect previously migrated data to Drupal data. The example below lookups up the taxonomy term ID based on the source reference (topic_area_id) from a previously ran migration. Note: You will need to add a migration dependency to your migration yml file to make sure migrations are run in the correct order.

 field_resource_category:
   plugin: migration_lookup
   migration: nceo_migrate_resource_category
   no_stub: true
   source: topic_area_id

Solution

Without using a Drupal Core patch, we need a way to do a migration_lookup in a more manual way. Thankfully prepareRow in your Migrate source plugin makes this pretty easy.

Note: This is not a complete Migrate source plugin. All the methods are there, but I’m focussing on the prepareRow method for this post. The most important part of the code is manually querying the Migrate map database table created in the Paragraph Entity migration.

<?php
 
namespace Drupal\your_module\Plugin\migrate\source;
 
use Drupal\Core\Database\Database;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Row;
 
/**
* Source plugin for Sample migration.
*
* @MigrateSource(
*   id = "sample"
* )
*/
class Sample extends SqlBase {
 
 /**
  * {@inheritdoc}
  */
 public function query() {
   // Query source data.
 }
 
 /**
  * {@inheritdoc}
  */
 public function fields() {
   // Add source fields.
 }
 
 /**
  * {@inheritdoc}
  */
 public function getIds() {
   return [
     'item_id' => [
       'type' => 'integer',
       'alias' => 'item_id',
     ],
   ];
 }
 
 /**
  * {@inheritdoc}
  */
 public function prepareRow(Row $row) {
   // In migrate source plugins, the migrate database is easy.
   // Example: $this->select('your_table').
   // Getting to the Drupal 8 db requires a little more code.
   $drupalDb = Database::getConnection('default', 'default');
 
   $paragraphs = [];
   $results = $drupalDb->select('your_migrate_map_table', 'yt')
     ->fields('yt', ['destid1', 'destid2'])
     ->condition('yt.sourceid2', $row->getSourceProperty('item_id'), '=')
     ->execute()
     ->fetchAll();
   if (!empty($results)) {
     foreach ($results as $result) {
       // destid1 in the map table is the nid.
       // destid2 in the map table is the entity revision id.
       $paragraphs[] = [
         'target_id' => $result->destid1,
         'target_revision_id' => $result->destid2,
       ];
     }
   }
 
   // Set a source property that can be referenced in yml.
  // Source properties can be named however you like.
   $row->setSourceProperty('prepare_multiple_paragraphs', $paragraphs);
 
   return parent::prepareRow($row);
 }
 
}

In your migration yml file, you can reference the prepare_multiple_paragraphs that was created in the migrate source plugin like this:

id: sample
label: 'Sample'
source:
 plugin: sample
process:
 type:
   plugin: default_value
   default_value: your_content_type
 field_paragraph_field:
   source: prepare_multiple_paragraphs
   plugin: sub_process
   process:
     target_id: target_id
     target_revision_id: target_revision_id

Sub_process was formally the iterator plugin and allows you to loop over items. This will properly create references to multiple Paragraph Entities. It will be nice when the migration_lookup plugin can properly handle this use case, but it’s a good thing to understand how prepareRow can provide flexibility.

Feb 22 2018
Feb 22

When using traditional APIs your application is typically requesting or pulling data from an external service, requiring a request for fresh data if you want to see recent changes. When using webhooks, that process is reversed: data is pushed from an external service in real-time keeping your application more up to date and your project running more efficiently. Here are a few examples:

  • Facebook - Receive an alert anytime a message is read
  • Stripe - Get alerted anytime a transaction comes through
  • Eventbrite - Get alerted if an event is created or updated

This of course is not an exhaustive list; you'll need to check the application you are integrating with to see if they are implementing webhooks. A Google search like "Stripe Webhooks" is a good first step.

Implementing a webhook in your application requires defining a URL to which your webhook can push data. Once defined, the URL is added to the application providing the webhook. In Drupal 8, controllers are a straightforward way to define a path. See the complete code for an example.

When the webhook is fired it hits the defined URL with applicable data. The data that comes from a webhook is called the payload. The payload is often a JSON object, but be sure to check the application’s documentation to see exactly what you should be expecting. Capturing the payload is straightforward using the Request object available in a controller like this:

public function capture(Request $request) {
  $payload = $request->getContent();
}

If your payload is empty, you can always try some vanilla PHP:

Inspecting the Payload

Debugging webhooks can be a bit challenging if you are developing locally because your local environment typically does not have a public URL. Further, some webhooks require that the receiving URL implement SSL, which can also present challenges locally. The following options can help you navigate debugging webhooks locally.

Easiest

When capturing the payload, you can log it in Drupal. This option requires pushing your code up to a publicly available URL (a dev or staging environment).

$this->logger->debug('<pre>@payload</pre>', ['@payload' => $payload]);

Once you know what the payload looks like, you can copy it, modify it and make your own fake webhook calls locally using Postman. Feel free to checkout the importable Postman example in the repo.

Most Flexible

There is a utility called ngrok that allows you to expose your local environment with a publicly available URL; if you anticipate a lot of debugging it is probably worth the time to set up. Once ngrok is in place, you use the same logging method as above or use XDEBUG or something similar to inspect the payload. Ngrok will give you a unique, public URL which you can register, but which forwards to a server you have running on localhost. You can even use it with a local server that uses vhosts, such as yoursite.test with the command:

ngrok http -host-header=rewrite yoursite.test:80

Capturing and Processing the Payload

I'm a big fan of Drupal's queue system. It allows quick storage of just about anything (including JSON objects) and a defined way to process it later on a CRON run.

In your controller, when the payload comes in, immediately add the payload to your defined queue rather than processing it right away. This will make sure it is always running as efficiently as possible. You can of course process it right away if you choose to do so and skip the rest of this post.

$this->queue->createItem($payload);

Later when the queue runs, you can process the payload and do what you need to do, like create a node. Here is an example from the queue plugin (see ProcessPayloadQueueWorker.php for the full code):

 public function processItem($data) {   
   // Decode the JSON that was captured.
   $decode = Json::decode($data);
   // Pull out applicable values.
   // You may want to do more validation!
   $nodeValues = [
     'type' => 'machine_name_here',
     'status' => 1,
     'title' => $decode['title'],
     'field_custom_field' => $decode['something'],
   ];
 
   // Create a node.
   $storage = $this->entityTypeManager->getStorage('node');
   $node = $storage->create($nodeValues);
   $node->save();
 }

Once a queue is processed on CRON, the item is removed from the queue. Check out Queue UI module for easy debugging.

Security

As when building any web application, security should be a major consideration. While not an exhaustive list, here are a few things you can do to help make sure your webhook stays secure.

  • Check your service's webhook documentation to see what authentication protocols they provide.
  • Create your own token that only your application and the webhook service know about. If that is not included, do not accept the request. See the authorize method in the controller.
  • Instead of processing the payload and turning it into a node, consider doing an API call back to the service using the ID from payload and requesting the data to ensure its authenticity.
  • You should consider sanitizing content coming from the payload.

Once you implement a Webhook, you'll be hooked! Here's all the code packaged up.

There are of course Drupal contrib modules around webhooks. I encourage you to check them out, but if you have specific use cases or complex needs, rolling your own is probably the way to go.

May 04 2017
May 04

In Drupal 8, setting your sites domain in settings.php is no longer possible. In Drupal 7, you could set the base_url in settings.php like:

$base_url = 'http://domain.com';

Have you noticed in Drupal 8 that when you use drush uli it returns a url that starts with http://default! If you are tired of copying and pasting what comes after http://default/ or adding the --uri=http://domain.com flag along with drush uli I have a solution for you!

Meet the drushrc.php file. I prefer to put this one level higher than my Drupal root. So…

  • Project repo
    • webroot (public_html, web, docroot, etc)
    • drush/drushrc.php

Lots can go in the drushrc.php file, but if you simply want to fix the drush uli default issue, it can just have:

<?php
 
$options['uri'] = 'http://domain.com';

If you are using GIT to manage your code base, you could consider a strategy of a drushrc.php file per environment. Example:

Create drush/drushrc.local.php

That file can contain:

<?php
 
$options['uri'] = 'http://domain.dev';

Your main drushrc.php now looks like:

<?php
 
/**
 * If there is a local drushrc file, then include it.
 */
 
$local_drushrc = __DIR__ . "/drushrc.local.php";
if (file_exists($local_drushrc)) {
  include $local_drushrc;
}

Now you can place drush/drushrc.local.php in your .gitignore file.

If you are using a PaaS like Pantheon, you can take this strategy:

Since Pantheon automatically handles setting the $options[‘url’] for you, you can simply say...if NOT Pantheon, use my local dev domain.

With the Pantheon approach, your drushrc.php file can look like:

<?php
 
if (!isset($_SERVER['PANTHEON_ENVIRONMENT'])) {
  $options['uri'] = 'http://domain.dev';
}

I believe setting the $options[‘url’] has always been possible if using drush aliases, so continue on if you’ve always done that.

Now enjoy the infinite bliss when typing drush uli and having the correct domain returned.

Apr 27 2017
Apr 27

Working with external APIs in Drupal has always been possible. Using PHP’s curl function or drupal_http_request is simple enough, but in Drupal 8 you can build in a lot more flexibility using Drupal::httpClient. Drupal::httpClient leverages Guzzle, a PHP HTTP client. If your project requires more than a single interaction with an external API a few steps can be taken to make those interactions much more reusable.

Make a Drupal Service for your API Connection

In Drupal 8, you can turn reusable functionality into a Service that you can easily reuse in your other custom code. Services can be called in module hooks and added to things like controllers using Dependency Injection. Defining a service in Drupal 8 requires a services.yml file in your custom module. For example, let's create a module called my_api:

The code from this example is based on a couple real-world API clients we’ve created recently.

  1. Watson API - IBM’s Watson API allows interacting with powerful utilities like speech to text. Rather than using Drupal’s httpClient in this case, we found a PHP class that already did the heavy lifting for us and we created a Drupal Service to extend that PHP class. In this case, we only needed to use Watson’s text-to-speech functionality to save text to audio files in Drupal, but ended up creating a flexible solution to interact with more than just the text-to-speech endpoint. You can check that code out on Drupal.org or GitHub.
  2. PCO API - Planning Center Online (PCO) is a CRM that is popular with faith-based institutions. PCO has a well documented RESTful API. In this case, I was building an application that needed to access the people stored in Planning Center in a Drupal 8 application. You can check that code out on Drupal.org or GitHub.
my_api.services.yml
services:
  my_api.client:
    class: '\Drupal\my_api\Client\MyClient'
    arguments: ['@http_client', '@key.repository', '@config.factory']

The class indication references the PHP class for your service; arguments references the other Services being utilized in the Class.

Now MyClient can be referenced throughout our code base. In a Drupal hook (example hook_cron) it might look something like this:

$client = Drupal::service('my_api.client');
$client->request();

In a Controller (example MyController.php) it might look like this:

<?php
 
namespace Drupal\my_custom_module\Controller;
 
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\my_api\MyClient;
 
/**
 * Class MyController.
 *
 * @package Drupal\my_custom_module\Controller
 */
class MyController extends ControllerBase {
 
  /**
   * Drupal\my_api\MyClient definition.
   *
   * @var \Drupal\my_api\MyClient
   */
  protected $myClient;
 
  /**
   * {@inheritdoc}
   */
  public function __construct(MyClient $my_client) {
    $this->myClient = $my_client;
  }
 
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('my_api.client')
    );
  }
 
  /**
   * Content.
   *
   * @return array
   *   Return array.
   */
  public function content() {
    $this->myClient->request());
    return [];
  }
}

Now we can use a common method in my_api.client like “request” to make calls to our external API. Connect then serves as a nice wrapper for the httpClient service.

public function request($method, $endpoint, $query, $body) {
  $response = $this->httpClient->{$method}(
    $this->base_uri . $endpoint,
    $this->buildOptions($query, $body)
  );
}

The request method accepts: A method (GET, POST, PATCH, DELETE, etc.) Endpoint (the API being used) Query (querystring parameters) Body

You can adjust your method parameters to best fit the API you are working with. In this example, the parameters defined covered the needed functionality.

$request = $this->myClient->request('post', 'people/v2/people', [], $body);

Using httpClient directly would look something like:

$response = $this->httpClient->post(
  'http://someapi.com/people/v2/people',
  [
    'auth' => ['token', 'secret'],
    'body' => json_encode($body),
  ]
);

Using httpClient instead of a service directly could litter hard coded values throughout your code base and insecurely expose authentication credentials.

Next time your project requires integrating with a 3rd party API, consider turning it into a service to set yourself up well for the future.

Nov 28 2016
Nov 28

Controllers in Drupal 8 are the equivalent of hook_menu in Drupal 7. A controller lets you define a URL and what content or data should appear at that URL. If you’re like me, limiting access to my controllers is sometimes an afterthought. Limiting access is important because it defines who can and can’t see a page.

Controllers are defined in a YAML file called module_name.routing.yml. Access and permission rules are defined in the the module_name.routing.yml under _requirements. Most of the code examples will be from a module_name.routing.yml file added to my_module in the top level.

Note: There is a lot of existing documentation on how to create controllers in Drupal 8, so I won’t focus on that here.

I’ve outlined some of the most useful approaches for limiting access below. You can jump straight to the most relevant section using the following links: limit by permission, limit by role, limit by one-off custom code, limit by custom access service.

Limit by permission

In this case, a permission from the Drupal permissions page is given. Permissions can be found at /admin/people/permissions. Finding the exact permission name can be tricky. Look for module.permissions.yml files in the module providing the permission.

my_module.dashboard:
  path: 'dashboard'
  defaults:
    _controller: '\Drupal\my_module\Controller\DashboardController::content'
    _title: 'Dashboard'
  requirements:
    _permission: 'access content'

Key YAML definition:

_permission: 'THE PERMISSION NAME'

Limit by role

You can also limit access by role. This would be useful in cases where users of a specific role will be the only ones needing access to your controller. You can define user roles at /admin/people/roles.

my_module.dashboard:
  path: 'dashboard'
  defaults:
    _controller: '\Drupal\my_module\Controller\DashboardController::content'
    _title: 'Dashboard'
  requirements:
    _role: 'administrator'

Key YAML definition:

_role: 'THE ROLE NAME'

You can specify multiple roles using "," for AND and "+" for OR logic.

Limit by one-off custom code

In cases where you have custom access requirements, adding an access method to your controller might make sense. In this example, the page should not be viewed before a specified date.

my_module.dashboard:
  path: 'dashboard'
  defaults:
    _controller: '\Drupal\my_module\Controller\DashboardController::content'
    _title: 'Dashboard'
  requirements:
   _custom_access: '\Drupal\my_module\Controller\DashboardController::access

Key YAML definition:

_custom_access: '\Drupal\my_module\Controller\DashboardController::access

The access method in my controller would look like:

<?php
namespace Drupal\my_module\Controller;
 
use Drupal\Core\Access\AccessResult; 
use Drupal\Core\Controller\ControllerBase;
 
/**
 * Defines the Dashboard controller.
 */
class DashboardController extends ControllerBase { {
 
 /**
  * Returns content for this controller.
  */
 public function content() {
   $build = [];
   return $build;
 }
 
 /**
  * Checks access for this controller.
  */
 public function access() {
   // Don’t allow access before Friday, November 25, 2016.
   $today = date("Y-m-d H:i:s");
   $date = "2016-11-25 00:00:00";
   if ($date < $today) {
     // Return 403 Access Denied page.  
     return AccessResult::forbidden();
    }
    return AccessResult::allowed();
  }
}

Limit by custom access service

This is similar to having an access method in your controller, but allows the code to be reused across many controllers. This is ideal when you are doing the same access check across many controllers.

my_module.dashboard:
  path: 'dashboard'
  defaults:
    _controller: '\Drupal\my_module\Controller\DashboardController::content'
    _title: 'Dashboard'
  requirements:
    _custom_access_check: 'TRUE'

Key YAML definition:

_custom_access_check: 'TRUE'

Proving the _custom_access_check service requires creating two files in my_module.

my_module/my_module.services.yml (defines the Access service and where to find our Access class)

services:
  my_module.custom_access_check:
    class: Drupal\my_module\Access\CustomAccessCheck
    arguments: ['@current_user']
    tags:
      - { name: access_check, applies_to: _custom_access_check }

my_module/src/Access/CustomAccessCheck.php

<?php
namespace Drupal\my_module\Access;
 
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
 
/**
 * Class CustomAccessCheck.
 *
 * @package Drupal\my_module\Access
 */
class CustomAccessCheck implements AccessInterface {
 
  /**
   * A custom access check.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   Run access checks for the logged in user.
   */
  public function access(AccountInterface $account) {
    // User has a profile field defining their favorite color.
    if ($account->field_color->hasField() && !$account->field_color->isEmpty() && $account->field_color->getString() === 'blue') {
      // If the user's favorite color is blue, give them access.
      return AccessResult::allowed();
    }
    return AccessResult::forbidden();
  }
 
}

While the above covers some of the most useful ways to restrict access to a controller, there are additional options. Drupal.org has a couple of good resources including Structure of Routes and Access Checking on Routes.

Oct 20 2015
Oct 20

Recently, I needed to add some dynamic content to a Views footer. Specifically, I needed to change a link in the Views footer based on the current path, which isn’t an option from the Views UI. I found some good documentation showing how this can be done in older versions of Drupal (https://www.drupal.org/node/749452), but nothing for Drupal 8. I figured the approach must be similar in Drupal 8 so I started searching and reverse engineering.

First, I added a footer directly through the Views UI and exported my sites configuration using Drupal 8’s configuration management tools. Finding the footer in the exported YAML file provided some valuable insight on how I might add the footer programmatically.

Views Footer as found in the YAML export:

footer:
  area_text_custom:
    id: area_text_custom
    table: views
    field: area_text_custom
    relationship: none
    group_type: group
    admin_label: ''
    empty: false
    tokenize: false
    content: Footer content is great
    plugin_id: text_custom

The YAML Views export provided the settings I needed to add the footer, but I also needed to figure out the right place to do it in code. If you are familiar with Views hooks, then you know there are a ton of them and sometimes finding the right one to use is a bit of trial and error. Since some of the Drupal 7 examples I found used function hook_views_pre_view(), I started with that hook. Using the Devel module, a great option for debugging in Drupal, and its dpm() function, I inspected the $view object. In Drupal 8, dpm() shows the available methods for an object. With a little bit of guesswork, the setHandler method seemed like the correct choice.

Output from the dpm() function.Output from the dpm() function.

I was able to add a Views footer with some code in a custom module:

use Drupal\views\ViewExecutable;
function YOURMODULENAME_views_pre_view(ViewExecutable $view, $display_id, array &$args) {
  if ($view->id() == 'view_machine_name' && $display_id === 'view_display') {
    $options = array(
      'id' => 'area_text_custom',
      'table' => 'views',
      'field' => 'area_text_custom',
      'relationship' => 'none',
      'group_type' => 'none',
      'admin_label' => '',
      'empty' => TRUE,
      'tokenize' => FALSE,
      'content' => ‘Footer content is great.’,
      'plugin_id' => 'text_custom',
    );
    $view->setHandler('view_display', 'footer', 'area_text_custom', $options);
  }
}

One piece of the Drupal 8 Views module has been solved!

Oct 20 2015
Oct 20

Recently, I needed to add some dynamic content to a Views footer. Specifically, I needed to change a link in the Views footer based on the current path, which isn’t an option from the Views UI. I found some good documentation showing how this can be done in older versions of Drupal (https://www.drupal.org/node/749452), but nothing for Drupal 8. I figured the approach must be similar in Drupal 8 so I started searching and reverse engineering.

First, I added a footer directly through the Views UI and exported my sites configuration using Drupal 8’s configuration management tools. Finding the footer in the exported YAML file provided some valuable insight on how I might add the footer programmatically.

Views Footer as found in the YAML export:

footer:
  area_text_custom:
    id: area_text_custom
    table: views
    field: area_text_custom
    relationship: none
    group_type: group
    admin_label: ''
    empty: false
    tokenize: false
    content: Footer content is great
    plugin_id: text_custom

The YAML Views export provided the settings I needed to add the footer, but I also needed to figure out the right place to do it in code. If you are familiar with Views hooks, then you know there are a ton of them and sometimes finding the right one to use is a bit of trial and error. Since some of the Drupal 7 examples I found used function hook_views_pre_view(), I started with that hook. Using the Devel module, a great option for debugging in Drupal, and its dpm() function, I inspected the $view object. In Drupal 8, dpm() shows the available methods for an object. With a little bit of guesswork, the setHandler method seemed like the correct choice.

Output from the dpm() function.Output from the dpm() function.

I was able to add a Views footer with some code in a custom module:

use Drupal\views\ViewExecutable;
function YOURMODULENAME_views_pre_view(ViewExecutable $view, $display_id, array &$args) {
  if ($view->id() == 'view_machine_name' && $display_id === 'view_display') {
    $options = array(
      'id' => 'area_text_custom',
      'table' => 'views',
      'field' => 'area_text_custom',
      'relationship' => 'none',
      'group_type' => 'none',
      'admin_label' => '',
      'empty' => TRUE,
      'tokenize' => FALSE,
      'content' => ‘Footer content is great.’,
      'plugin_id' => 'text_custom',
    );
    $view->setHandler('view_display', 'footer', 'area_text_custom', $options);
  }
}

One piece of the Drupal 8 Views module has been solved!

Oct 12 2015
Oct 12

Our client, Human Rights Watch, publishes a large volume of content and with the redevelopment of HRW.org, they wanted a way to curate their content in more meaningful ways. One way we did this was by showing what content is shared most frequently across a variety of social networks.

Keeping up with the number of shares across multiple social networks seemed like an uphill battle. A system like this would require creating a background process that looks up and stores the number of shares for HRW.org URLs. It would also require writing separate API integrations for each social network HRW wants to target and maintaining them over time.

Since we didn’t want to create and maintain a custom social share count aggregation system if we didn’t have to, we started looking at 3rd party solutions. HRW.org is a multi-lingual site, so some of the options weren’t viable since they provide the most shared items for a domain without taking into account the language of the content. We selected SharedCount.com, which has a simple API that allowed us to quickly build a system to determine most shared content. The system sends SharedCount a batch of URLs and it returns a JSON object with the number of shares across a variety of social networks for each URL.

Here is a sample JSON response from the API: https://docs.sharedcount.com/v1.0/docs/bulk-1

{
    "data": {
        "http://google.com/": {
            "StumbleUpon": null,
            "Pinterest": 1003223,
            "Twitter": 11400,
            "LinkedIn": 95,
            "Facebook": {
                "commentsbox_count": 10117,
                "click_count": 265614,
                "total_count": 9476803,
                "comment_count": 1793601,
                "like_count": 1500762,
                "share_count": 6182440
            },
            "GooglePlusOne": 7710780
        },
        "http://stackoverflow.com/": {
            "...snip...": "98 URLs not shown for brevity..(snip).."
        }
 
    },
    "_meta": {
        "urls_completed": 100,
        "bulk_id": "a4f8f0fd436995987dbef98bbff9accc61282c63",
        "completed": true,
        "urls_queued": 100
}

How it Works

HRW.org is built with Drupal. In a custom module, we set up several Drush commands that can run at intervals. We use Jenkins to run this every 10 minutes, so we can provide a nearly real-time look at what is happening socially with HRW’s content. The workflow looks like this:

  1. Query a batch of URLs in Drupal. Currently, it sends any URL published in the last 30 days. To make this easy for HRW to adjust, we used Drupal Views to determine which URLs to send to the ShareCount API.
  2. Send a batch of URLs to the SharedCount bulk API https://docs.sharedcount.com/v1.0/docs/bulk.
  3. Process a JSON object from SharedCount, which includes the number of shares for each URL.
  4. Store the results in a simple database table with the URL, number of shares and language of the content.
  5. Query the custom database table to show most shared content in a custom Drupal block on the HRW.org homepage. Screenshot of the most shared content block from hrw.org

Hooray for leveraging all that social data in a simple, but meaningful way.

Aug 05 2015
Aug 05

Human Rights Watch (HRW) has been sharing important stories for over 30 years and we were excited about enhancing the digital story-telling experience in the latest relaunch of HRW.org. Out-of-the-box, Drupal provided a great platform for us to craft tools that matched HRW’s internal publishing workflows, while allowing their publishers to create long-form content with videos, galleries and other rich content.

Embedded Media

Aten's design team set the bar with the new design direction for HRW.org. Their vision for the long-form articles included flexibility for embedding media content throughout the text. After a lengthy evaluation period with Drupal’s Media module, it become clear that HRW needed more flexibility than it provided. In addition to embedding file based media (i.e. images), there were requirements around embedded text based content like quotes and callouts for other node based content. For consistency, we went with the Node Embed approach that we’ve blogged about before. Having a node-based workflow also provided a familiar workflow for HRW’s publishers.

Example of node embedded content on HRW.org

Long-form Reports

One of HRW’s most important tasks is creating in-depth reports on certain issues around the world. It's not uncommon for these reports to approach 100 pages In PDF form. Two goals of the relaunch included the report creation workflow in Drupal and improving report navigation.

Example of a report on HRW.org

Report Creation Workflow

HRW has an internal workflow for editing and publishing reports that they’ve used for years. The end product of that workflow is a printed report and an online HTML version. Rather than trying to force a new internal workflow, we discussed pain points HRW had with their current process and built a couple tools to provide a better experience.

HTML Upload

When creating report nodes in Drupal, HRW can upload the HTML file that was created outside of the content management system. The upload process adds anchor tags and generates a table of contents based on semantic heading tags, detects image references that do not exist on the site yet, and makes a few other formatting changes The report content and table of contents are then saved in the database rather than processing the uploaded content on the fly.

Report Navigation

In past iterations, reports were broken up into many pages. Now, users can use the table of contents to quickly jump to relevant sections of the report. Having reports on a single page also allows users to use built in browser tools like “Find” to search for keywords.

Example of a report table of contents on HRW.org

Featured Content

HRW has many topic and location based landing pages where editors can curate content. HRW wanted the flexibility to manually feature content or, given the large number of these pages, have the most relevant automatically show up. To achieve this, we used Views, Entity Reference, and a custom module to tie the two together. The custom code checks the Entity Reference field for manually curated content then utilizes Views to pull the relevant content into the remaining content slots available. This allows HRW to mix curated and automated content easily without having to choose one approach over the other.

Example of featured content on HRW.org

Related Content

Rather than showing users additional content at the bottom of an article based on time or tag, we worked with HRW to craft an algorithm, or scoring system, to display related content. When deciding which relevant content to show, date, geography, and topic are all taken into consideration. Similar to featured content, HRW has the flexibility to manually relate content on any article with the algorithmic suggestions filling missing slots.

Example of related content on HRW.org

Sharelines

Once HRW publishes important information, they leverage social channels like Twitter to help spread the word. We provided HRW publishers the ability create curated Sharelines that are displayed prominently for users to Tweet directly from the article.

Example of sharelines on HRW.org

It was exciting to work on such a large publishing project and help provide tools to help tell important stories in many languages. We hope you enjoy browsing the new HRW.org!

Feb 06 2015
Feb 06

Views is an indispensable and powerful module at the heart of Drupal that you can use to quickly generate structured tables or lists of consistently formatted content, and filter and group that content by simple or complex logic. But in pushing Views to do ever more complex and useful things, we can sort of paint ourselves into a corner sometimes. For instance, I have many times created multiple Views displays on a single page that contain overlapping content. My homepage has a Views display of manually curated content, using Nodequeue or a similar module. On the same homepage, I have a Views display of news content that shows the most recent content. Since the two different Views displays pull from the same bucket of content, it is very possible to have duplicate content across the displays. Here is an example:

Before image showing overlapping content.

Notice the underlined duplicate titles across the two Views displays.

This is what we want:

After image showing the deduped Views display.

Notice the missing featured titles from the deduped Views display.

By creating a custom Drupal module and utilizing a Views hook, we can remove the duplicate content across the two Views displays. We programmatically check exactly which pieces of content are in one View, and we feed that information to a filter in the second View that excludes it.

Before diving into my example, I want to cover a few assumptions I’m making about you.

  • You are using Drupal 7
  • You are familiar with Views module
  • You know how to install modules
  • You know at least a touch of PHP

Steps to Follow Along

View Example Code on Github

Step 1

My example code assumes that you have created two Views displays.

  • Featured - A View display of manually curated content. This display will be used to generate a list of content to exclude from our automated Views display.
  • Automated - A View display of news content that shows the most recent content. This display will accept a list of content to be excluded.

You can of course adapt the Views displays to your exact needs.

After creating the Views you wish to use, you’ll need to know the machine name of the View and View display.

One way to retrieve these names is from the view edit URL. While editing your view, notice the URL:

/admin/structure/views/view/automated_news/edit/block

In my case, automated_news is the view name and block is the view display name.

Make a note of your machine names for Step 3

Step 2

On the view you wish to dedup or exclude content from, you’ll need to add and configure a contextual filter.

  1. Navigate to edit the automated content view
  2. Under “Advanced” & “Contextual Filters”, click add and select “Content: Nid (The node ID.)”
  3. Select “Provide default value” and choose “Fixed value”.
  4. Leave the Fixed value empty as we’ll provide this in code
  5. Under “More” select “Allow multiple values” and “Exclude”
  6. Save the view

Step 3

Enable your custom module that contains the deduping code. You are welcome to download the example module on Github and use it, or add the code to an existing custom module if it makes more sense. In any case, you’ll need to customize the module a little bit to work with your Views.

  1. Update the machine name variables from Step 1. See $featured_view_name, $featured_view_display, $automated_view_name and 2. $automated_view_display
  2. Save your module
  3. Enable your module
  4. Clear your Drupal cache

If everything was configured correctly, you should see your Views displays properly deduped.

Code Explained

View Example Code on Github

The code relies on hook_views_pre_view(), a Views hook. Using this hook, we can pass values to the Views display contextual filter set in Step 2. Here is a version where content IDs (NIDs) 1, 2, 5 & 6 are manually being passed to a view for exclusion.

/**
  * @implements hook_views_pre_view().
  *
  * https://api.drupal.org/api/views/views.api.php/function/hook_views_pre_view/7
  */
function hook_views_pre_view(&$view, &$display_id, &$args){
  // Check for the specific View name and display
  if ($view->name == ‘automated_news’ && $display_id == ‘block’) {
    $args[] = 1+2+5+6;
  }
}

There are many ways you could dynamically build a list of NIDs you wish to exclude. In my example, we are loading another Views display to build a list of NIDs. The function views_get_view() loads a Views display in code and provides access to the result set.

// Load the view
// https://api.drupal.org/api/views/views.module/function/views_get_view/7
$view = views_get_view('automated_news');
$view->set_display('block');
$view->pre_execute();
$view->execute();
 
// Get the results
$results = $view->result;

Drupal Views is a powerful module and I like the ability to extend it even further using the extensive Views hooks API. In the case of my example, we can keep using Views with writing complex database queries.

Oct 23 2014
Oct 23

We build Drupal sites with a combination of site code and the settings that Drupal stores in the database. Settings are easy for someone with no coding experience to change; but we can't track setting changes in the database as easily as we can track changes in code.

Drupal’s Features module is the most widely adopted solution in Drupal 7 for storing settings as version-controlled configuration in code. Like with most things Drupal, there isn’t just one approach to configuration in code: a few Aten folks have been working on another approach called CINC.

If you do decide to use the Features module, you’ll quickly learn there isn’t a single way of creating features. Drupal Kit provides some guidelines, but structuring and organizing Features-created modules is largely left up to the developer. Things can quickly get unwieldy on a complex site with multiple developers and many Features. In cases where Features is a project requirement, we’ve created a process that has worked well for us.

Be consistent with Features naming conventions

Our Feature names follow this convention: [projectshortname][summary][package_name]_feature

  • [projectshortname] This three-character code is decided at the beginning of a project and keeps the custom module and feature names unique to the project.
  • [summary] This is a super-short summary of the specifics of the feature.
  • [package_name] This should closely follow the package naming convention set for the project. Keep reading to learn more about package names.
  • feature This lets others know that this module was created by Features and also helps keep the module name unique.

Examples in practice

  • Page content type - abc_page_entity_feature
  • Image style definitions - abc_image_styles_config_feature
  • Blog View - abc_blog_views_feature

Categorize Features by providing a package name

When creating a new Feature, you can specify a package name. This is the same as defining “package = [something]” in a custom module .info file. The Package name groups your feature on the Features list page and the overall modules page. Being consistent with package names makes it easier for other developers and clients to find available features. We suggest nailing down package names at the beginning of a project. Our package names typically look something like this:

  • [projectshortname] Configuration (image styles, text formats, search settings, various module settings)
  • [projectshortname] Entity (content types, fields, field collections, taxonomies, etc.)
  • [projectshortname] Views (views defined by views module)
  • [projectshortname] Page (page manager & panels)

Create a directory structure for modules created by Features

Our typical modules directory (sites/all/modules) is structured like this:

  • contrib (modules downloaded from Drupal.org)
  • custom (modules that aren’t contrib and specific to the project)
  • features (modules created by Features)
  • patched (patched contrib modules)

The Features directory (sites/all/modules/features) is then broken down a bit further to make it easier to find what you need. We try to make this mirror package names as much as possible.

  • features
    • configuration
    • entity
      • content_type
      • field_collection
      • shared
      • taxonomy
    • page
    • views

Limit cross-Feature dependencies

It is normal for a Feature to be dependent on other Drupal modules. For example, a content type Feature will be dependent on the Field Group module if using field groups. When creating content type Features, fields used by the content type are tightly coupled with each feature. The quickest way to a cross-Feature dependency is by creating two content type Features that have several shared fields (e.g. body, tags). Content Type One may contain the field base for the body field. Content Type Two also uses the body and now has a dependency on Content Type One.

Cross-Feature dependencies make it hard to have Features that are truly independent and reusable across projects. Our way around this is being very intentional about when we use shared fields and adding them in a completely different Feature. We call this Feature “Shared Field Base”. This shared Feature allows Content Type One and Content Type Two to be completely independent of one another.

At the end of the day, the important thing is to pick an approach and stick with it throughout the project. We’ve created a process that works well for us, but there are other approaches. How does your approach differ from ours? What other tips do you have for creating features and keeping them organized? Are you excited about Drupal 8’s plans for configuration in code?

Dec 20 2013
Dec 20

Drupal has long struggled in the area of media management and embedding. Despite Drupal’s wide selection of media modules, none of them have matched our requirements (probably too much to ask). Media module almost tries to do too much thus being a little overwhelming and challenging to modify. The Insert and Video Embed modules, among others, attempt to solve specific use cases, but are quite limited. While developing cpr.org (Colorado Public Radio), we were able to really spend some time making Node Embed work well. While it's not without issues, I’m excited to share how we’ve used the Node Embed module at Aten to help solve our media management needs.

Node Embed allows you to embed one node into another. In this post, I’ll outline what it takes to set up Node Embed to embed images into your content. The same concept could be used for audio, video, or whatever content you can dream up to embed.

Before going any further, I want to give a shout out to Aten's Scott Reynen who is the current maintainer of Node Embed and has added some of the functionality to Node Embed mentioned in this post.

Embed an image with a caption and credit

This tutorial is based on Drupal 7.x

1. Download & enable necessary modules

I'm assuming you know how to download and enable modules.

Required Modules

  • Views and its dependencies

Optional modules

  • WYSIWYG (Node Embed supports CKeditor or FCKeditor) WYSIWYG is not required but makes it easy for content editors to embed nodes directly from the WYSIWYG toolbar.

2. Verify text formats

  • Navigate to /admin/config/content/formats
  • Edit the text format profile you want to enable Node Embed for
  • Enable “Insert Node” by checking the checkbox
  • You may need to experiment with Filter processing order, but should work out of the box (typically first in the list)

Note: If using with filtered html, make sure you are allowing tags!

3. Setup content types

For this example I’ll assume you have at least Basic Page and Image content types.

Basic page content type

  • Title Field
  • Body Field

Image content type

  • Title
  • Photo caption
  • Credits
  • Image field

4. Embed a node

After completing the first three steps, you are ready to embed a node.

  • Create a new Image node (with an image of course). Take note of the NID (Node ID) after saving the node. NIDs can easily be found in the URL when editing a node. Note: If using the Node Embed button with WYSIWYG, you can avoid manually finding Node IDs.
  • Create a new Basic Page node. While creating or editing the Basic Page node, embed your Image node. The format is [[nid:(node_id)]] Replace “(node_nid)” with your actual node ID. ex. [[nid:1]]
  • Save the node. You should now have an embedded image. By default, Node Embed will use the “Default” view mode for the content you are embedding.

Your embedded content may not look exactly like you want at this point, so continue on to learn how to customize it.

Customizing embedded content

There are at least three options for taking control of your embedded content.

1. Customize the Node Embed view mode

By default, Node Embed module provides a Node Embed view mode for every content type. It just needs to be enabled. Until the Node Embed view mode is enabled, it will use the default view mode. A module like Display Suite will give you additional control over the output of your view mode.

  • Visit /admin/structure/types
  • Click “Manage Display” for the Image content type
  • Enable Node Embed view Mode (This setting is hidden in the vertical tabs at the bottom of the page)
  • Adjust the Node Embed view Mode (set image style, etc.)

2. Create a node embed template in your theme

Creating a custom node template will give you complete control over your markup.

sites/all/themes/[custom_theme]/node--[content_type_machine_name]--node_embed.tpl.php

You can put whatever html you like in your node--[content_type_machine_name]--node_embed.tpl.php file. Here is a very simple example that will show the image field if it exists:

<?php if(isset($content['field_image'])): ?>
<figure><?php print render($content['field_image']); ?></figure>
<?php endif; ?>

Note: Using the render function will still use some view mode settings like image styles and whether labels show or not.

3. Overwrite the default view mode

By default Node Embed uses a view mode called node-embed.

If you have a different view mode you prefer to use simply adjust your Node Embed code. For example, the following Node Embed code would use the teaser view mode.

[[nid:2 view_mode=teaser]]

Passing in custom variables

Node embed allows you to pass variables that you can access in preprocess functions and templates.

Imagine needing to easily be able to left or right align images. Your node embed code changes slightly to:

[[nid:1 align=right]]

Note: align is a made up variable. It could be anything, and same for the value right.

Accessing the Node Embed Parameter in code

You can access the parameters using the following construct in a node preprocess function.

$variables['node_embed_parameters']['align']

Add a node preprocess function to template.php

<?php
/**
 * Override or insert variables into the node template.
 */
function yourthemename_preprocess_node(&$variables) {
  if ($variables['view_mode'] == 'node_embed' && $variables['type'] == 'image') {
 
    // Check to see if the align Node Embed parameter exists
    if(isset($variables['node_embed_parameters']['align'])) {
      // For security, run align through the check_plain() function
      // Add the alignment value to the classes array used in our node template
      $variables['classes_array'][] = check_plain($variables['node_embed_parameters']['align']);
    }
 
  }
}
// Closing php included only for proper highlighting in blog post
?>

Add the following to node--image--node_embed.tpl.php

<?php
/**
 * $classes variable will include the alignment value of right, left or whatever is passed in
 */
?>
 
<figure class="<?php print $classes; ?> image-wrapper">
  <?php if(isset($content['field_image'])): ?>
  <?php print render($content['field_image']); ?>
  <?php endif; ?>
 
  <?php if(isset($content['body'])): ?>
  <figcaption><?php print render($content['body']); ?></figcaption>
  <?php endif; ?>
 
  <?php if(isset($content['field_credits'])): ?>
  <div class="image-credits"><?php print render($content['field_credits']); ?></div>
  <?php endif; ?>
</figure>

Finally, add some css style.css (or whatever stylesheet being used)

//add the potential classes that someone may use in Node Embed
.right {
  float: right;
  padding-left: 20px;
}
.left {
  float: left;
  padding-right: 20px;
}

Note: The Aten team will hopefully release our custom module that allows you to dynamically setup Node Embed Parameters and allows your users to easily insert an image with alignment, etc. Hat tip to Rob Ballou for his excellent work there. Here is a sneak peek:

Custom Node Embed Options

The module provides "Align" and "Do not format or resize" to the Node Embed WYSIWYG insert form, making it easy to add custom parameters in a user friendly way. These options are customizable per content type.

Caveats

  • Migrating content containing node embed tags would cause the embedded content to break if the node IDs don’t match on the new environment.
  • Node Embed inserts custom tags into your markup [[nid:(node_id)]]. This won’t translate well if ever migrating away from Drupal or Node Embed.

Troubleshooting

  • If just [[nid:(node_id)]] is outputting to the screen, make sure the node you are embedding exists and is published.
  • Double check text formats to make sure Node Embed tags (image tag included) are getting processed and not filtered out

Call for Comments

What are your thoughts on this approach to media handling in Drupal? What else has or hasn't worked for you?

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