Upgrade Your Drupal Skills

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

See Advanced Courses NAH, I know Enough
Mar 13 2020
Mar 13

Drupal 9 release date has been pinned for June 3, 2020, and it's coming up super fast. What does that mean for your site?

First of all, don't panic. Drupal 7 and 8 end of life are scheduled until November 2021, so there is plenty of time to upgrade. However it is always good to plan ahead with time and take advantage of the new features and security releases with the new version.

If you are on D7

Moving to Drupal 9 will be very similar as moving to Drupal 8, and in fact, there is no reason to wait, and the recommendation is to move to D8 as soon as possible, incorporating the tools described in the next section to search for possible incompatibilities.

Dec 17 2019
Dec 17

Developer portals need three kinds of great content to succeed. 

  1. API Documentation

  2. Technical Documentation

  3. Marketing Content

Content is being consumed differently now than it was in the past. Dense long-form, bespoke technical communication is going away. Clear, highly visual , easy to read content is taking the lead.  When building a developer portal, content quality and organization needs to be a high priority for your team. .   

API Documentation

Oct 28 2019
Oct 28

Recently one of our clients asked us to come up with a better language detection and redirection solution for their multilingual Drupal 8 site. Most out of the box solutions do not provide a great user experience and IP based detection does not always work as expected. Browser-based redirection is also not an ideal option since at some point a visitor might want to manually choose which language they would like to see.

Having this issue in hand I started looking into possible solutions, I looked at a number of multilingual Drupal and non-Drupal sites and could not find anything that would work for our client. I thought what if we ask a visitor what to do by showing them a box with browser detected language. This is just like Chrome's translation prompt that asks you if you would like to translate the site. The prompt that is very simple and not as annoying as some auto-redirect solutions.

May 10 2019
May 10

On several occasions clients have come to us asking for a form with a gated resource. They want the  resource (.pdf, .doc, .xls etc.) to be available to download after the user fills out a form. In exchange for the visitor's valuable information, we provide them a file.

This is easily achievable in Drupal 8 with the Webform and Token modules.  At the time of creation of this blog post, I used Drupal version 8.7.0.

Nov 06 2018
Nov 06

Pattern Lab (PL), a commonly known pattern library, is an open-source project to generate a design system for your site. In the last two years it has gotten a lot of attention in the Drupal community. It's a great way to implement a design system into your front-end workflow.

The following post describes how our client (the City and County of San Francisco) began to implement a pattern library that will eventually be expanded upon and re-used for other agency websites across the SF.gov ecosystem.

USWDS

Using the U.S. Web Design System (USWDS), until their own pattern library was ready for prime time, was a client requirement.

May 14 2018
May 14

Commenting system giant Disqus powers reader conversations on millions of sites, including large publishers like Rolling Stone and the Atlantic. So when Disqus quietly introduced ads into their free plans last year, there was some understandable frustration.

Why did @disqus just add a bunch of ads to my site without my permission? https://t.co/CzXTTuGs67 pic.twitter.com/y2QbFFzM8U

— Harry Campbell (@TheRideshareGuy) February 1, 2017

 

Apr 26 2018
Apr 26

Most corporate websites have a career section where people can find out what jobs are open. These sections can range from just a few standing openings to hundreds of vacant positions across multiple job types that open and close frequently. Larger companies traditionally use HR software systems to manage this ebb and flow.

One popular and powerful HR system is Taleo. Many companies simply link offsite to the taleo.com listing, which serves as the company’s only place to see and apply to openings. Other times, that same listing gets embedded on the company’s career section. As you can see in the user journey below for our client MemorialCare, this got the job done, but didn’t really convey a company’s brand or culture.

Apr 11 2018
Apr 11

I presented at DrupalCon Nashville about working with the City of San Francisco to make a better transaction experience for residents. Moving beyond a simple content site where we tell users how to do things, we are now developing a brand new city website in Drupal 8, where residents can actually do those things online. The presentation covers how to run an agile project of this scale in a government environment, what we did as a part of discovery, where we're going, and how our foray into design & development is progressing so far.

Here are the slides to digest. Don't hesitate to reach out if you have any questions!

Mar 19 2018
Mar 19

With all the changes in Drupal 8, it’s no wonder the landscape for access control modules is adapting. While the port of Organic Groups has started, there are several major issues to resolve before it’s ready for use. There are, however, a couple promising new options.

Permissions By Term

If you don’t need group management, Permissions by Term is the Drupal 8 substitute for the Taxonomy Access Control module. It controls node visibility based on the how a node is tagged. Term access is granted by role, and individual users can be whitelisted for term access permissions.

Jan 29 2018
Jan 29

Dreditor is a beloved and indispensable tool, in the form of a browser extension, that enhances project issue pages on Drupal.org. When it comes to reviewing patches, it turns what would just be a plain text file into a feature rich interface for reviewing patches, allowing users to easily select and comment on lines of code, which then get pasted into the comment form, as properly formatted HTML.

I've been lucky to have some extra community time here at Chapter Three over the past couple of weeks. While perusing the core issue queue, I decided to resurrect an old User Style I created for Dreditor back in 2009, and give Dreditor a little refresh. You can install the Dreditor Refresh style with Stylish here. Make sure you also have Dreditor installed, and have logged into Drupal.org.

Oct 16 2017
Oct 16

Last week at DrupalCamp Quito, I presented an updated, Spanish-language version of my DrupalCon session. If you would like to view the presentation in English, you can find it on my DrupalCon blog post.

Las estructuras orientadas a objetos han reemplazado a nuestros queridos "hooks" que nos permitían extender Drupal con nueva funcionalidad sin necesidad de hackear core (u otros módulos de contrib). Pero, ¿cómo funciona esto? En esta charla revisamos cómo extender un módulo para implementar single sign-on (SSO), y al hacerlo nos adentramos a cómo la programación orientada a objetos hace magia en nuestros módulos, haciéndolos más fáciles de escribir, entender y depurar. Adicionalmente, se describen algunos de los patrones de diseño de Drupal, cómo utilizar event listeners, sobreescribir rutas y otras herramientas.

[embedded content]

Slides: https://goo.gl/QgskXw
Código de ejemplo: https://github.com/arlina-espinoza/openid_fb

Sep 07 2017
Sep 07

Recently I had to generate term-specific aliases (aliases that are different from the default alias pattern set for Article entities). This is how to do it:

1. Enable the Pathauto module
2. Set the default URL alias pattern for your content type in order to fire the hook
3. Implement hook_pathauto_alias_alter() in your .module file.

Example module structure:

mymodule/
  - mymodule.info.yml
  - mymodule.module
  - src/
    - ArticlePathAlias.php

I like to keep .module clean and simple and because of that I store the main logic in src/ArticlePathAlias.php file.

The mymodule.info.yml this is just a regular .info file.

4. Add the following to your mymodule.module file:

Aug 26 2017
Aug 26

Sometimes you might want to display additional data in the autocomplete results, for instance add content language next to the title, or display entity type or any other related data. In this blog post I will demonstrate how to alter suggestions in autocomplete fields in Drupal 8. The project is available for download from github, see the link at the bottom of the page.

Link autocomplete results

Here is the module structure I will be using:

Aug 25 2017
Aug 25

Sometimes you might want to display additional data in the autocomplete results, for instance add content language next to the title, or display entity type or any other related data. In this blog post I will demonstrate how to alter suggestions in autocomplete fields in Drupal 8. The project is available for download from github, see the link at the bottom of the page.

Link autocomplete results

Here is the module structure I will be using:

alter_entity_autocomplete/
  - alter_entity_autocomplete.info.yml
  - alter_entity_autocomplete.services.yml
  - src/
    - EntityAutocompleteMatcher.php
    - Controller/
      - EntityAutocompleteController.php
    - Routing/
      - AutocompleteRouteSubscriber.php

Contents of the alter_entity_autocomplete.info.yml file:

name: Alter Entity Autocomplete
description: The module alters entity autocomplete suggestion list.
type: module
core: 8.x

Contents of the alter_entity_autocomplete.services.yml file:

services:

  alter_entity_autocomplete.route_subscriber:
    class: Drupal\alter_entity_autocomplete\Routing\AutocompleteRouteSubscriber
    tags:
      - { name: event_subscriber }

  alter_entity_autocomplete.autocomplete_matcher:
    class: Drupal\alter_entity_autocomplete\EntityAutocompleteMatcher
    arguments: ['@plugin.manager.entity_reference_selection']

Contents of the src/EntityAutocompleteMatcher.php file. This is the file where you would change the output of the sugesstions/autocomplete results:

<?php

namespace Drupal\alter_entity_autocomplete;

use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Tags;

class EntityAutocompleteMatcher extends \Drupal\Core\Entity\EntityAutocompleteMatcher {

  /**
   * Gets matched labels based on a given search string.
   */
  public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') {

    $matches = [];

    $options = [
      'target_type'      => $target_type,
      'handler'          => $selection_handler,
      'handler_settings' => $selection_settings,
    ];

    $handler = $this->selectionManager->getInstance($options);

    if (isset($string)) {
      // Get an array of matching entities.
      $match_operator = !empty($selection_settings['match_operator']) ? $selection_settings['match_operator'] : 'CONTAINS';
      $entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10);

      // Loop through the entities and convert them into autocomplete output.
      foreach ($entity_labels as $values) {
        foreach ($values as $entity_id => $label) {

          $entity = \Drupal::entityTypeManager()->getStorage($target_type)->load($entity_id);
          $entity = \Drupal::entityManager()->getTranslationFromContext($entity);

          $type = !empty($entity->type->entity) ? $entity->type->entity->label() : $entity->bundle();
          $status = '';
          if ($entity->getEntityType()->id() == 'node') {
            $status = ($entity->isPublished()) ? ", Published" : ", Unpublished";
          }

          $key = $label . ' (' . $entity_id . ')';
          // Strip things like starting/trailing white spaces, line breaks and tags.
          $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key)))));
          // Names containing commas or quotes must be wrapped in quotes.
          $key = Tags::encode($key);
          $label = $label . ' (' . $entity_id . ') [' . $type . $status . ']';
          $matches[] = ['value' => $key, 'label' => $label];
        }
      }
    }

    return $matches;
  }

}

Contents of the src/Controller/EntityAutocompleteController.php file:

<?php

namespace Drupal\alter_entity_autocomplete\Controller;

use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\alter_entity_autocomplete\EntityAutocompleteMatcher;
use Symfony\Component\DependencyInjection\ContainerInterface;

class EntityAutocompleteController extends \Drupal\system\Controller\EntityAutocompleteController {

  /**
   * The autocomplete matcher for entity references.
   */
  protected $matcher;

  /**
   * {@inheritdoc}
   */
  public function __construct(EntityAutocompleteMatcher $matcher, KeyValueStoreInterface $key_value) {
    $this->matcher = $matcher;
    $this->keyValue = $key_value;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('alter_entity_autocomplete.autocomplete_matcher'),
      $container->get('keyvalue')->get('entity_autocomplete')
    );
  }

}

Here is contents of the src/Routing/AutocompleteRouteSubscriber.php file:

<?php

namespace Drupal\alter_entity_autocomplete\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;

class AutocompleteRouteSubscriber extends RouteSubscriberBase {

  public function alterRoutes(RouteCollection $collection) {
    if ($route = $collection->get('system.entity_autocomplete')) {
      $route->setDefault('_controller', '\Drupal\alter_entity_autocomplete\Controller\EntityAutocompleteController::handleAutocomplete');
    }
  }

}

This module was developed with help of my co-workers and I hope you will find this useful.

Download from Github

Aug 18 2017
Aug 18

In this post I will show you a technique to fix HTML issues, import images or perform content operations during migrations.

We have to fix source content before most content migrations. This can be challenging if there are many entries in the source database. The powerful Drupal 8 Migration API provides elegant ways to solve this type of problem.

To solve HTML issues, I always create my own process plugin. Here is an example how you would call your own process plugin to fix HTML issues in the body field:

Aug 03 2017
Aug 03

In this post I will show a custom process plugin that I created to migrate taxonomy terms. The plugin handles the creation of new terms and prevents duplicates.

Below is a portion of the migration template. In the example, I am migrating new terms into keywords vocabulary via field_keywords field.

  field_keywords:
    -
      plugin: existing_term
      # Destination (Drupal) vocabulary name
      vocabulary: keywords
      # Source query should return term name
      source: term_name
    -
      plugin: skip_on_empty
      method: row

This is the source code for the process plugin.

Jul 28 2017
Jul 28

WordPress to Drupal 8

In this post I will show you how to migrate thumbnail content from Wordpress to Drupal 8. My goals are to help you better understand the content migration process, give you starting point for future migrations, and teach you how to write process plugins and migration sources. Taxonomy terms and users migration is more straightforward so I won't cover it here.

This migration example contains templates to migrate thumbnails content. For this post, I assume the image/thumbnail field is using the Media module field. I will be using the Migrate drush module to run migrations.

First, make sure to configure your connection in your settings.php file. Add the following with proper credentials:

Jul 27 2017
Jul 27

WordPress to Drupal 8

In this post I will show you how to migrate thumbnail content from Wordpress to Drupal 8. My goals are to help you better understand the content migration process, give you starting point for future migrations, and teach you how to write process plugins and migration sources. Taxonomy terms and users migration is more straightforward so I won't cover it here.

This migration example contains templates to migrate thumbnails content. For this post, I assume the image/thumbnail field is using the Media module field. I will be using the Migrate drush module to run migrations.

First, make sure to configure your connection in your settings.php file. Add the following with proper credentials:

$databases['migrate']['default'] = [
    'driver'   => 'mysql',
    'database' => 'wordpress_dbname',
    'username' => 'wordpress_dbuser',
    'password' => 'wordpress_dbpassowrd',
    'host'     => '127.0.0.1',
];

Here is the module structure I will be using:

wp_migration/
  - wp_migration.info.yml
  - wp_migration.module
  - migration_templates/
    - wp_content.yml
    - wp_thumbnail.yml
    - wp_media.yml
  - src/
    - Plugin/
      - migrate/
        - process/
          - AddUrlAliasPrefix.php
          - DateToTimestamp.php
        - source/
          - SqlBase.php
          - Content.php
          - Thumbnail.php

Contents of the wp_wordpress.info.yml file:

name: Wordpress Migration
type: module
description: Migrate Wordpress content into Drupal 8.
core: 8.x
dependencies:
  - migrate
  - migrate_drupal
  - migrate_drush

Contents of the migration_templates/wp_thumbnail.yml file:

id: wp_thumbnail
label: 'Thumbnails'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_thumbnail
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
  constants:
    # This path should point ot WP uploads directory.
    source_base_path: '/path/to/source/wp/uploads'
    # This is directory name in Drupal where to store migrated files
    uri_file: 'public://wp-thumbnails'
process:
  filename: filename
  source_full_path:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/source_base_path
        - filepath
    -
      plugin: urlencode
  uri_file:
    -
      plugin: concat
      delimiter: /
      source:
        - constants/uri_file
        - filename
    -
      plugin: urlencode
  uri:
    plugin: file_copy
    source:
      - '@source_full_path'
      - '@uri_file'
  status: 
    plugin: default_value
    default_value: 1
  changed: 
    plugin: date_to_timestamp
    source: post_date
  created: 
    plugin: date_to_timestamp
    source: post_date
  uid: 
    plugin: default_value
    default_value: 1
destination:
  plugin: 'entity:file'
migration_dependencies:
  required: {}
  optional: {}

Contents of the migration_templates/wp_media.yml file:

id: wp_media
label: 'Media'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_thumbnail
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
  constants:
    bundle: image
process:
  bundle: 'constants/bundle'
  langcode:
    plugin: default_value
    default_value: en
  'field_image/target_id':
    -
      plugin: migration
      migration: wp_thumbnail
      source: post_id
    -
      plugin: skip_on_empty
      method: row
destination:
  plugin: 'entity:media'
migration_dependencies:
  required: {}
  optional: {}

Contents of the migration_templates/wp_content.yml file:

id: wp_content
label: 'Content'
migration_tags:
  - Wordpress
source:
  plugin: wordpress_content
  # Wordpress post type (custom variable)
  post_type: post
  # This is WP table prefix  (custom variable)
  # DB table example: [prefix]_posts
  table_prefix: wp
process:
  type:
    plugin: default_value
    default_value: article
  'path/pathauto':
    plugin: default_value
    default_value: 0
  'path/alias':
    # This will add the following to URL aliases in Drupal
    plugin: add_url_alias_prefix
    # url/alias/prefix/2017/07/21/[post-title]
    prefix: url/alias/prefix
    source: path_alias
  promote: 
    plugin: default_value
    default_value: 0
  sticky: 
    plugin: default_value
    default_value: 0
  langcode:
    plugin: default_value
    default_value: en
  status: 
    plugin: default_value
    default_value: 1
  title: post_title
  created: 
    plugin: date_to_timestamp
    source: post_date
  changed: 
    plugin: date_to_timestamp
    source: post_modified
  field_image:
    -
      plugin: migration
      migration: wp_media
      source: thumbnail
    -
      plugin: skip_on_empty
      method: row
  'body/summary': post_excerpt
  'body/format':
    plugin: default_value
    default_value: full_html
  'body/value': post_content
destination:
  plugin: 'entity:node'
migration_dependencies:
  required: {}
  optional: {}

Contents of the src/Plugin/migrate/process/AddUrlAliasPrefix.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;

/**
 * Add prefix to URL aliases.
 *
 * @MigrateProcessPlugin(
 *   id = "add_url_alias_prefix"
 * )
 */
class AddUrlAliasPrefix extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    $prefix = !empty($this->configuration['prefix']) ? '/' . $this->configuration['prefix'] : '';
    return $prefix . $value;
  }

}

Contents of the src/Plugin/migrate/process/DateToTimestamp.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\process;

use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;

/**
 * Date to Timetamp conversion.
 *
 * @MigrateProcessPlugin(
 *   id = "date_to_timestamp"
 * )
 */
class DateToTimestamp extends ProcessPluginBase {

  /**
   * {@inheritdoc}
   */
  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
    return strtotime($value . ' UTC');
  }

}

I like to keep my module code clean and organized so I use base classes that I later extend in individual migration source files.

Here is contents of the src/Plugin/migrate/source/SqlBase.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate\Row;

class SqlBase extends DrupalSqlBase {

  /**
   * Get database table prefix from the migration template.
   */
  protected function getPrefix() {
    return !empty($this->configuration['table_prefix']) ? $this->configuration['table_prefix'] : 'wp';
  }

  /**
   * Get Wordpress post type from the migration template.
   */
  protected function getPostType() {
    return !empty($this->configuration['post_type']) ? $this->configuration['post_type'] : 'post';
  }

  /**
   * Generate path alias via pattern specified in `permalink_structure`.
   */
  protected function generatePathAlias(Row $row) {
    $prefix = $this->getPrefix();
    $permalink_structure = $this->select($prefix . '_options', 'o', ['target' => 'migrate'])
      ->fields('o', ['option_value'])
      ->condition('o.option_name', 'permalink_structure')
      ->execute()
      ->fetchField();
    $date = new \DateTime($row->getSourceProperty('post_date'));
    $parameters = [
      '%year%'     => $date->format('Y'),
      '%monthnum%' => $date->format('m'),
      '%day%'      => $date->format('d'),
      '%postname%' => $row->getSourceProperty('post_name'),
    ];
    $url = str_replace(array_keys($parameters), array_values($parameters), $permalink_structure);
    return rtrim($url, '/');
  }

  /**
   * Get post thumbnail.
   */
  protected function getPostThumbnail(Row $row) {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_postmeta', 'pm', ['target' => 'migrate']);
    $query->innerJoin($prefix . '_postmeta', 'pm2', 'pm2.post_id = pm.meta_value');
    $query->fields('pm', ['post_id'])
      ->condition('pm.post_id', $row->getSourceProperty('id'))
      ->condition('pm.meta_key', '_thumbnail_id')
      ->condition('pm2.meta_key', '_wp_attached_file');
    return $query->execute()->fetchField();
  }

}

Contents of the src/Plugin/migrate/source/Thumbnail.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate\Row;

/**
 * Extract content thumbnails.
 *
 * @MigrateSource(
 *   id = "wordpress_thumbnail"
 * )
 */
class Thumbnail extends SqlBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_postmeta', 'pm', ['target' => 'migrate']);
    $query->innerJoin($prefix . '_postmeta', 'pm2', 'pm2.post_id = pm.meta_value');
    $query->innerJoin($prefix . '_posts', 'p', 'p.id = pm.post_id');
    $query->fields('pm', ['post_id']);
    $query->fields('p', ['post_date']);
    $query->addField('pm2', 'post_id', 'file_id');
    $query->addField('pm2', 'meta_value', 'filepath');
    $query
      ->condition('pm.meta_key', '_thumbnail_id')
      ->condition('pm2.meta_key', '_wp_attached_file')
      ->condition('p.post_status', 'publish')
      ->condition('p.post_type', 'post');
    return $query;
  }

  /**
   * {@inheritdoc}
   */
  public function fields() {
    return [
      'post_id'   => $this->t('Post ID'),
      'post_date' => $this->t('Media Uploaded Date'),
      'file_id'   => $this->t('File ID'),
      'filepath'  => $this->t('File Path'),
      'filename'  => $this->t('File Name'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getIds() {
    return [
      'post_id' => [
        'type'  => 'integer',
        'alias' => 'pm2',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
    $row->setSourceProperty('filename', basename($row->getSourceProperty('filepath')));
  }

}

Contents of the src/Plugin/migrate/source/Content.php file:

<?php

namespace Drupal\wp_migration\Plugin\migrate\source;

use Drupal\migrate\Row;

/**
 * Extract content from Wordpress site.
 *
 * @MigrateSource(
 *   id = "wordpress_content"
 * )
 */
class Content extends SqlBase {

  /**
   * {@inheritdoc}
   */
  public function query() {
    $prefix = $this->getPrefix();
    $query = $this->select($prefix . '_posts', 'p');
    $query
      ->fields('p', [
        'id',
        'post_date',
        'post_title',
        'post_content',
        'post_excerpt',
        'post_modified',
        'post_name'
      ]);
    $query->condition('p.post_status', 'publish');
    $query->condition('p.post_type', $this->getPostType());
    return $query;
  }

  /**
   * {@inheritdoc}
   */
  public function fields() {
    return [
      'id'            => $this->t('Post ID'),
      'post_title'    => $this->t('Title'),
      'thumbnail'     => $this->t('Post Thumbnail'),
      'post_excerpt'  => $this->t('Excerpt'),
      'post_content'  => $this->t('Content'),
      'post_date'     => $this->t('Created Date'),
      'post_modified' => $this->t('Modified Date'),
      'path_alias'    => $this->t('URL Alias'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getIds() {
    return [
      'id' => [
        'type'  => 'integer',
        'alias' => 'p',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function prepareRow(Row $row) {
    // This will generate path alias using WP alias settings.
    $row->setSourceProperty('path_alias', $this->generatePathAlias($row));
    // Get thumbnail ID and pass it to the wp_media migration plugin.
    $row->setSourceProperty('thumbnail', $this->getPostThumbnail($row));
  }

}

IMPORTANT: You must run migrations in their proper order. In this example you have to run wp_thumbnail first, wp_media second and wp_content last.

Comments Not Loading?

Due to some temporarily SSL cert issue please refresh the page using this link in order to be able to leave comments.

Jul 18 2017
Jul 18

AppNexus + Drupal 8

One of our clients requested to integrate AppNexus ads with their Drupal 8 site. The requirements were very simple:

  • Ability to display ads as blocks.
  • Ad blocks should be configurable: tagId, Sizes. And pass those variables via apntag.defineTag().
  • Ability to pass global page options via apntag.setPageOpts().

Since there wasn't any module that would support this I decided to create a module that will be very easy to use and at the same time will be extendable and useful in other projects.

The module comes with a simple PHP class that helps to generate pageOpts from hook_page_attachments(). You may create your own class using that class as an example.

Jul 15 2017
Jul 15

Amp and Drupal 8

Adding AMP (Accelerated Mobile Pages) support could be very tricky and complex depending on your project needs. In this blog post I won't be using AMP module. Instead I created something more simpler and easier to use for AMP integration. The Simple AMP module is still using Lullabot's AMP PHP library. The module is a starter module and most likely won't be very useful out of the box, but it can get you going very fast. You may need to extend it and this shouldn't as hard as in other modules.

Download Simple AMP

Jul 13 2017
Jul 13

Adding AMP support could be very tricky and complex depending on your project needs. In this blog post I won't be using AMP module. Instead I created something more simple and easier to use for simple AMP integration. The Simple AMP module is still using Lullabot's AMP PHP library. The module is a starter module and most likely won't be very useful out of the box, but it can get you going very fast. It is available for download here https://github.com/chapter-three/simple_amp.

First thing you need to do is to install composer require lullabot/amp, the module won't work without this library.

Once you install the module modify few lines of code in the module:

  • Modify Drupal\simple_amp\AmpBase::getMetadata() to reflect data that you would like to show in AMP metadata.
  • Modify template in simple_amp/templates/amp.html.twig to match your design needs.
  • You may also have custom template per content type: amp--node.html.twig or amp--node--article.html.twig

By default Simple AMP module includes only basic AMP JS files and in case if you need to add other AMP JS libraries you would have to add some code.

In order to add additional AMP JS files you will have to modify Drupal\simple_amp\AmpBase::detect() method. You would need to write some regular expressions to detect ads, youtube videos, vimeo, instagram or any other libraries. Once detected you would add that library to protected $scripts = [] array in the AmpBase class.

Example:

protected function detect() {
    $youtube = [
      '/youtube\.com\/watch\?v=([a-z0-9\-_]+)/i',
      '/youtube\.com\/embed\/([a-z0-9\-_]+)/i',
      '/youtu.be\/([a-z0-9\-_]+)/i',
      '/youtube\.com\/v\/([a-z0-9\-_]+)/i',
    ];
    foreach ($youtube as $regexp) {
      if (preg_match($regexp, $this->html)) {
        $this->scripts[] = '<script async custom-element="amp-youtube" src="https://cdn.ampproject.org/v0/amp-youtube-0.1.js"></script>';
      }
    }
  }

Roadmap

  • Improve UI
  • Eliminate any need for code modifications.

Feel free to create pull requests, help is always appreciated.

Jun 01 2017
Jun 01

The editorial experience is a critical and often overlooked part of website development. A weak editorial experience means slower site updates. Without a robust and intuitive content creation process, editors lose motivation to keep their site up to date. Our modular content creation process makes it quick and easy for editors to create content while providing front-end users with a rich visual experience.

What Are Modular Content Components?

Modular Content Components are a powerful way for website editors to quickly add new sections and send article content to multiple distribution channels in a single step. Drupal sites with Modular Content Components make it easy to add new distribution channels and send custom formats to each.

Apr 21 2017
Apr 21

Wouldn't it be great if???

Configuration management is one of the most useful site development features in Drupal 8. It makes a site's configuration exportable, importable and manageable in git. Whilst building the configuration management feature, a thought that often occurred was "Wouldn't it be great if you can take an existing set of configuration and install a new site from it?". Every Drupal developer has turned up to a new project and had to learn a different way to build a development site. Do you get the code from github? Download a database from production or some other special location? And is that database sanitised? I often wonder how many Drupal developers there are in the world wandering around with production databases on their laptops.

Apr 10 2017
Apr 10

In the previous post I covered how to use the Component Libraries module and Twig to create simple reusable components, using an SVG sprite icon template as an example.

Part 2: Applying Twig Concepts to Write Better Code

When learning Drupal theming, overriding templates is one of the key topics of interest. It’s a simple thing. Copy the source template into your theme, modify it and clear the cache. Easy! However, doing just that over and over again, can lead to a mess of unmaintainable code.

Apr 04 2017
Apr 04

Twig is a powerful templating engine with many useful features. In the context of Drupal, especially when coming from a Drupal 7 way of doing things, it can be easy to overlook many of these features in the interest of just getting the job done quickly.  However, taking the time to learn Twig beyond just printing variables is worthwhile, because it can help you solve common problems.  

In this series, we will explore some of the tools Twig itself provides, within the context of a Drupal 8 theme to improve the quality of the code we write. Each post will identify a common problem, and provide details about Twig concepts that can be used to help solve them.

Mar 08 2017
Mar 08

Acquia announced Monday that Chapter Three was one of its top three growth partners in 2016!

Chapter Three has developed and hosted multi-site installations for large companies like CooperVision and Memorial Care on the Acquia platform for over four years, and we partner with Acquia to host many more sites.

"Chapter Three partners with Acquia to create enterprise class solutions," explains Chapter Three managing partner, John Faber. "We use the Acquia platform for our Drupal 8 sites, supporting our clients by supporting each other."

Dec 09 2016
Dec 09

Motivation

Using hook_views_query_alter to alter a queryWhen you first hear of Drupal's hook_views_query_alter you think you get it. You understand Drupal's hook system (presumeably, otherwise start here), you've used other hooks before, maybe you even experience a twinge of guilty excitement that it's one of the hooks that hasn't been removed from Drupal 8. You think to yourself, okay, this function will be passed a query argument, and I'll alter it -- and you're right! But it turns out it's a little more complicated than it sounds.

Oct 31 2016
Oct 31

Our minds and hearts were full as BADCamp 2016 came to a close. It was overflowing with excellent presentations and the best of the community. Our team led a Drupal 8 training, presented in the Front End Summit, and held a few presentations and sessions along the way. Here are a few of the recordings:

The Slice Template

Zakiya Khabir and Nica Lorber

Oct 11 2016
Oct 11

How to use Drush CommandsIn this blog post I will provide an example of a method I use to deploy changes on the projects that I work on my own time. This method is very useful when there is a need to make a lot of manual changes to the site when there is a new release. You could use this method to automate deployment process of your Drupal sites. In fact this method can be used for any other non-Drupal sites too.

For this blog post I am using Drush commands.

Lets start form a very simple example. I will use Acquia's directory structure to descibe where I am storing release scripts.

Oct 04 2016
Oct 04

Using Jupyter with Drupal 8Jupyter notebooks are used in data science for exploring / documenting / presenting data. If you haven't heard of Jupyter maybe you have by it's former name, iPython notebook. Since there are multiple "kernels" supported it was renamed to Jupyter ( for Julia-Python-R ). There is support for 3rd party kernels such as the Ruby kernel and thanks to the Jupyter-PHP project on github we can add PHP to the list.

Sep 06 2016
Sep 06

Custom Drush Commands for Drupal 8 and Drupal 7It is very easy to create your own custom Drush commands. In this blog post I will show two examples for Drupal 7 and Drupal 8.

Creating custom Drush commands could be very useful when you need to import data into Drupal or export data from Drupal. You would have to create a separate bash script where you would execute your custom command and add the bash script to your crontab.

Drupal 8 example

Drupal 8 module structure:

drush_example/
  - drush_example.info.yml
  - drush_example.drush.inc

drush_example.info.yml:

Aug 07 2016
Aug 07

(This module was tested on drupal 8.1.8)

Twig Extensions, Views and Drupal 8

Problem to solve

Sometimes working with Views can be a challenge, in some cases you want to modify the values or process the content of the field.

Many developers use the preprocess functions, or alter the query of the view, however there is another option: twig filters.

The Goals:

Being able to calculate the reading time of any given node (body field).

The field needs to output: "5 min read", "9 min read", depending on the word count of the node.

Twig Filters:

Twig filters give us the ability to modify variables on twig template.

Jun 27 2016
Jun 27

Paragraphs has become a popular site building tool for Drupal. In the feedback to our recent blog post, some asked why the Chapter Three team has not fully embraced the module. Most of our Drupal 8 sites use Entity Construction Kit with Inline Entity Form (ECK/IEF) to achieve what others do with Paragraphs.

Jun 13 2016
Jun 13

For the past two to three years, we’ve been evolving a flexible design solution for marketing pages. We call it the Slice Template.

What is the Slice Template?

The Slice Template is kind of like a layer cake. It’s  a design structure that enables site authors  to create web pages out of modular, horizontal components that span the width of the browser. Popularized by responsive design, this pattern has become a web standard. That’s because it can support a myriad of content.

Our team started calling these components slices early on. You could call them that too. We can make it our little thing :)

Jun 09 2016
Jun 09

Basic Configuration ManagementWhat is CMI?

One of the major features of Drupal 8 is the Configuration Management Initiative (CMI). I’ll briefly touch on why CMI is so important, but our own Alex Pott has written extensively on the creation, principles and importance of this feature.

Previous to Drupal 8, it was cumbersome to trade configuration between sites and environments. Moving configuration changes from a developmental environment into production meant either making the changes manually through the UI, or utilizing an advanced option like Features. A third option would be full-database swapping, but that can be quite risky and is generally considered poor-form.

May 31 2016
May 31

We Give a HootPerformance

As our team of impassioned front-end developers hammer their anvils in harmony, they’re putting influential principles in motion. We code with grace. We stride through our text editors with elegance and poise. Our minds and fingers fly with synchronicity in a display you could only describe as “truly inspiring”.

Maybe that’s not exactly the case, but we’re certainly passionate about what we do. Our team cares about the quality of work we produce, and about the principles we employ. We craft with great discretion and intention. Sometimes with grace, and sometimes with gallons of cold brew and thrash metal.

May 30 2016
May 30

How to Improve Performance with HTTP cache headersIn this blog post I will show you a simple technique to improve your web application performance by modifying headers. Please keep in mind, if you're using HTTP reverse proxy caching applications such as Varnish you might harm your application performance or your settings could be ignored.

This technique could help improve page loads for authenticated users where reverse proxy caching disabled.

Important: Drupal already already provides all the required headers when Performance settings properly configured. This is information is generic and could be very helpful for decoupled projects or any other frameworks.

May 27 2016
May 27

Easy Drupal Development with Drush ScriptingWhether you work on one Drupal site or multiple, it is often necessary for your local dev environment to be slightly different from your site's server.  Perhaps you need to disable Secure Pages because you don't want to set up SSL on your local environment, or there are modules specific to your website's server config.  If you work on multiple sites in a sporadic fashion its possible you need to synchronize your local database with the dev server between tasks, that way you aren't missing any updated configurations.

Sure, you can pull this off manually by grabbing the database, reloading your local, and updating your Drupal site's config; but why not add a little automation to help out?

May 26 2016
May 26

How to Register an Event Subscriber in Drupal 8Events in Drupal 8 allow for different components of the system to interact and communicate with each other. One system component dispatches the event at an appropriate time; many events are dispatched by Drupal core and the Symfony framework in every request. Other system components can register as event subscribers; when an event is dispatched, a method is called on each registered subscriber, allowing each one to react.

Most of the hooks from previous versions of Drupal were removed in Drupal 8 in favor of Events. Example: hook_init() or hook_boot() which now can be done by registering an event subscriber.

I will use the following structure for the example module:

May 24 2016
May 24

In this blog post I will briefly overview some of the very useful HTTP response header parameters that will help to secure any website. HTTP Response headers are name-value pairs of strings sent back from a server with the content you requested. More information can be found on the internet.

How to Secure Drupal HTTP Headers

I will cover some of the most important security-related HTTP parameters. The original blog post was written by Scott Helme who is the creator of SecurityHeaders.io. This is a brief overview of his blog post to introduce this technique to our readers.

Pages

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