Upgrade Your Drupal Skills

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

See Advanced Courses NAH, I know Enough
Jun 04 2016
Jun 04

Tom Friedhof

Senior Software Engineer

Tom has been designing and developing for the web since 2002 and got involved with Drupal in 2006. Previously he worked as a systems administrator for a large mortgage bank, managing servers and workstations, which is where he discovered his passion for automation and scripting. On his free time he enjoys camping with his wife and three kids.

Jun 03 2016
Jun 03

On a recent project we had to create a section that is basically a Twitter search for a hashtag. It needed to be usuable in different sections of the layout and work the same. Also, we were using the Paragraphs module and came up with a pretty nifty (we think) solution of creating a custom field that solved this particular problem for us. I will walk you through how to create a custom field/widget/formatter for Drupal 8. There are Drupal console commands for generating boilerplate code for this... which I will list before going through each of the methods for the components.

Field Type creation

The first thing to do is create a custom field. In a custom module (here as "my_module") either run drupal:generate:fieldtype or create a file called HashTagSearchItem.php in src/Plugin/Field/FieldType. The basic structure for the class will be:



namespace Drupal\my_module\Plugin\Field\FieldType;

use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\TypedData\DataDefinition;


class HashtagSearchItem extends FieldItemBase {



}

Next, implement a few methods that will tell Drupal how our field will be structured. Provide a default field settings for the field that will be the count for the amount of tweets to pull. This will return of default settings keyed by the setting's name.



  
  public static function defaultFieldSettings() {
    return [
      'count' => 6
    ] + parent::defaultFieldSettings();
  }

Then provide the field item's properties. In this case there will be an input for hashtag and a count. Each property will be keyed by the property name and be a DataDefinition defining what the properties will hold.


  
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
    $properties = [];
    $properties['hashtag_search'] = DataDefinition::create('string')
      ->setLabel(t('The hashtag to search for.'));
    $properties['count'] = DataDefinition::create('integer')
      ->setLabel(t('The count of twitter items to pull.'));
    return $properties;
  }

Then provide a schema for the field. This will be the properties that we have created above.


  
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
    return [
      'columns' => [
        'hashtag_search' => [
          'type' => 'varchar',
          'length' => 32,
        ],
        'count' => [
          'type' => 'int',
          'default' => 6
        ]
      ]
    ];
  }

Field widget creation

Next create the widget for the field, which is the actual form element and it's settings. Either drupal:generate:fieldwidget or create a file in src/Plugin/Field/FieldWidget/ called HashtagSearchWidget.php. This is the class' skeleton:



namespace Drupal\my_module\Plugin\Field\FieldWidget;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;



class HashtagSearchWidget extends WidgetBase {
  
}

Then implement several methods. Provide a default count of tweets to pull for new fields and the settings form for the field item:


  
  public static function defaultSettings() {
    return [
      'default_count' => 6,
    ] + parent::defaultSettings();
  }

  
  public function settingsForm(array $form, FormStateInterface $form_state) {
    $elements = [];
    $elements['default_count'] = [
      '#type' => 'number',
      '#title' => $this->t('Default count'),
      '#default_value' => $this->getSetting('default_count'),
      '#empty_value' => '',
      '#min' => 1
    ];

    return $elements;
  }

  
  public function settingsSummary() {
    $summary = [];
    $summary[] = t('Default count: !count', array('!count' => $this->getSetting('default_count')));

    return $summary;
  }

Then create the actual form element. Add the hashtag textfield and count number field and wrap it in a fieldset for a better experience:


  
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $item = $items[$delta];

    $element['hashtag_search'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Hashtag'),
      '#required' => FALSE,
      '#size' => 60,
      '#default_value' => (!$item->isEmpty()) ? $item->hashtag_search : NULL,
    ];

    $element['count'] = [
      '#type' => 'number',
      '#title' => $this->t('Pull count'),
      '#default_value' => $this->getSetting('default_count'),
      '#size' => 2
    ];

    $element += [
      '#type' => 'fieldset',
    ];

    return $element;
  }

In part 2, Bez will show you how to pull the tweets and create a field formatter for the display of the tweets. You can read that post here!

May 17 2016
May 17

Actually, we never left. We didn't stop building Drupal sites, even through the long release cycle. However, we did move our company website, activelamp.com, off of Drupal about 18 months ago. Our company site had been built on Drupal since the Drupal 4.7 days. That was back when it started to become uncool to write and maintain your own home-grown CMS. I eventually found Drupal, ditched my custom CMS, and never looked back.

Our site started on Drupal 4.7, upgraded onto Drupal 5, then Drupal 6, and also Drupal 7 all at the beginning of the release cycles of Drupal. About 18 months ago, when our site was in dire need of an update, we evaluated Drupal 8 but realized with no release date in sight, and the fact that we did not want to chase HEAD and develop on unstable API's, we decided to go a different route and build our updated site on Jekyll, a popular static generator. It's more fun to tinker with new technology when working on non-billable stuff, which is what we did. We brushed up on our Ruby skills and built out a Jekyll site (which is this site you're looking at if you're reading this blog post before Q3 of 2016).

We're getting ready for another update to our company website and moving back to Drupal to do it. Jekyll was great, but it came with its disadvantages over something like Drupal. This post will highlight some of the advantages and disadvantages of working with Jekyll the past 18 months, as well as highlight why we're excited to put activelamp.com on Drupal 8 in Q3 of this year.

Getting off the Island

If you've been around the Drupal community for a few years, you've probably heard the phrase "Get off the island". There was, and still is, a big push to bring other technologies into the Drupal stack and rid ourselves of NIH Syndrome -- Not Invented Here Syndrome.

We as a team took this movement quite literally and started doing more than just Drupal. We started to take on projects utilizing the Full Stack Symfony Framework, Laravel, AngularJS, Ember, Express / Node, Hapi, and Jekyll. We had successfully gotten off the island, so to speak, and it felt good. We decided to build activelamp.com on Jekyll, it has several advantages over using a CMS like Drupal.

Advantages of Static Generators

Having a statically generated site has huge advantages. Let's review a few of them:

Performance / Scalability

You don't need a complex hosting setup to host your site. We are currently hosting activelamp.com on S3, a simple storage service provided by Amazon Web Services. In fact, several months after we launched activelamp.com, we built a Jekyll site for Riot Games hosted on S3. The start of season League of Legends site we built on Jekyll handled millions of requests per day, hosted on AWS S3. Not bad for such a highly trafficked site. No moving parts equals a fast site.

Security

Since Jekyll sites are static HTML, there isn't a backend to exploit. There are no scripts that actually run on the server. This means you don't have to stay up-to-date with security updates -- there are none.

Structured Content

The final output of a Jekyll site is a static HTML site, but we still have structure when creating content. On activelamp.com, we have blog content, video content, job postings, etc. We add content using Markdown, with a little bit of YAML at the top of the file, and place the files into specific folders in our document tree. Jekyll compiles the site from a set of HTML templates, YAML, and Markdown files. Our content is written into discrete files and compiled on build. Since our content has semantic structure, we are still able to compose pages together with whatever content we want, we just need to write a Ruby plugin to do so. Which leads us to the disadvantages.

Disadvantages of Not Using a CMS

We found ourselves spending lots of time writing Ruby plugins when we wanted Jekyll to act more like a CMS. A few of the disadvantages we faced with our site on Jekyll include:

Editor Experience Sucks

If you have non-technical people on your team that want to contribute, there is a high barrier to entry. We have a few non-developers on our team, and it would be so much nicer if they didn't have to use Markdown to write blog posts for our site. The rich experience you can have with Drupal 8 and CKEditor is top notch, something we're missing using Jekyll. Running Jekyll, our non-technical users needed to learn how to compile the site to preview changes and also had to learn how to use git to submit their blog posts for review before publishing them. Jekyll is great for developers, not for non-developers.

Have to Write Code for Everything

Not that I have anything against writing code, but I've been spoiled by the Drupal community. For the most part, there is likely a module for anything that you want to accomplish. If there isn't a module in the wild, there is a huge community behind Drupal that will hopefully contribute to a new module that you put out, continually improving it (Plus, I would much rather code PHP than Ruby).

No Backend, No Interactions.

I listed no backend as an advantage above under Security, but you really can't do anything interactive without a backend. Our activelamp.com Jekyll site actually has a small backend written with Node JS. We have a small Express app that handles the forms and social streams, and a small Handlebars app that calls out to Google Analytics to create the most popular posts lists on blog category pages. Our site isn't 100% static, it's just not possible unless you truly do want a brochure type site where users are just consuming content, not interacting.

Excited for Drupal 8

We have been building on Drupal 8 since last December. We launched a portion of a site on Drupal 8 a couple months ago, and we're launching a full site in a few weeks on Drupal 8. Drupal development has become exciting again.

Our new website is going to call for more interactivity with our users (premium content, client portal, partner portal, etc...). It's in our best interest to go back to a platform where we don't have to code every feature that we want. Another advantage for going back to Drupal 8 is that we'll get to setup a nice content publishing workflow for ourselves again. Jekyll was fine, but we've built some pretty nice workflows for our clients, it would be nice to get an easier workflow into our internal processes too, to relieve the tension for the non-developers on our team.

Most importantly, Drupal 8 is fun to develop on. The OOP approach to writing modules, and leveraging composer packages is amazing. Drupal has definitely taken a step in the right direction. In my opinion, as Drupal 8 gains traction it will become the de facto standard for Enterprise CMS needs.

May 14 2016
May 14

Tom Friedhof

Senior Software Engineer

Tom has been designing and developing for the web since 2002 and got involved with Drupal in 2006. Previously he worked as a systems administrator for a large mortgage bank, managing servers and workstations, which is where he discovered his passion for automation and scripting. On his free time he enjoys camping with his wife and three kids.

May 07 2016
May 07

Drupal 8 has greatly improved editor experience out-of-the-box. It comes shipped with CKEditor for WYSIWYG editing. Although, D8 ships with a custom build of CKEditor and it may not have the plugins that you would like to have or that your client wants to have. I will show you how to add new plugins into the CKEditor that comes with Drupal 8.

Adding plugins with a button

First, create a bare-bones custom module called editor_experience. Files will be added here that will tell Drupal that there is a new CKEditor plugin. Find a plugin to actually install... for the first example I will use bootstrap buttons ckeditor plugin. Place the downloaded plugin inside libraries directory at the root of the Drupal installation; or use a make file to place it there. Also make sure you have the libraries module installed drupal module:download libraries.

Create a file inside of the editor_experience module inside of src/Plugin/CKEditorPlugin called BtButton.php. Add the name space and the two use statements shown below.





namespace Drupal\editor_experience\Plugin\CKEditorPlugin;

use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\editor\Entity\Editor;


class BtButton extends CKEditorPluginBase {

  ... 

}

The annotation @CKEditorPlugin tells Drupal there is a plugin for CKEditor to load. For the id, use the name of the plugin as defined in the plugin.js file that came with the btbutton download. Now we add several methods to our BtButton class.

First method will return false since it is not part of the internal CKEditor build.





public function isInternal() {
  return FALSE;
}

Next method will get the plugin's javascript file.





public function getFile() {
  return libraries_get_path('btbutton') . '/plugin.js';
}

Let Drupal know where your button is. Be sure that the key is set to the name of the plugin. In this case btbutton.





  public function getButtons() {
    return [
      'btbutton' => [
        'label' => t('Bootstrap Buttons'),
        'image' => libraries_get_path('btbutton') . '/icons/btbutton.png'
      ]
    ];
  }

Also implement getConfig() and return an empty array since this plugin has no configurations.

Then go to admin/config/content/formats/manage/basic_html or whatever format you have that uses the CKEditor and pull the Bootstrap button icon down into the toolbar.

Now the button is available for use on the CKEditor!

Adding plugins without a button (CKEditor font)

Some plugins do not come with a button png that allows users to drag the tool into the configuration, so what then?

In order to get a plugin into Drupal that does not have a button, the implementation of getButtons() is a little different. For example to add the Font/Font size dropdowns use image_alternative like below:





 public function getButtons() {
   return [
     'Font' => [
       'label' => t('Font'),
       'image_alternative' => [
         '#type' => 'inline_template',
         '#template' => '{{ font }}',
         '#context' => [
           'font' => t('Font'),
         ],
       ],
     ],
     'FontSize' => [
       'label' => t('Font Size'),
       'image_alternative' => [
         '#type' => 'inline_template',
         '#template' => '{{ font }}',
         '#context' => [
           'font' => t('Font Size'),
         ],
       ],
     ],
   ];
}

Then pull in the dropdown the same way the Bootstrap button plugin was added! Have any questions? Comment below or tweet us @activelamp.

Apr 28 2016
Apr 28
what-is-a-style-guide

One of our talented designers recently spoke at an event hosted by Drupal 4 Gov covering everything you need to know about style guides. Check out his presentation below as well as our Drupal page.  

The post Style Guides: What They Consist Of, The Benefits, And How To Get Started appeared first on .

Apr 22 2016
Apr 22
Evan has a passion for design, is a born problem solver, and loves technology. Evan has a bachelor of science in graphic design from the Art Institute, and has been designing since the turn of the century. Evan leads the creative efforts of ActiveLAMP, and ensures the stuff we build aesthetically looks good.

In his free time, Evan enjoys going to the gym, spending time with his wife and daughter, reading, and long walks on the beach.</span>

Apr 09 2016
Apr 09

Tom Friedhof

Senior Software Engineer

Tom has been designing and developing for the web since 2002 and got involved with Drupal in 2006. Previously he worked as a systems administrator for a large mortgage bank, managing servers and workstations, which is where he discovered his passion for automation and scripting. On his free time he enjoys camping with his wife and three kids.

Apr 04 2016
Apr 04

Tom Friedhof

Senior Software Engineer

Tom has been designing and developing for the web since 2002 and got involved with Drupal in 2006. Previously he worked as a systems administrator for a large mortgage bank, managing servers and workstations, which is where he discovered his passion for automation and scripting. On his free time he enjoys camping with his wife and three kids.

Jan 27 2016
Jan 27

Tom Friedhof

Senior Software Engineer

Tom has been designing and developing for the web since 2002 and got involved with Drupal in 2006. Previously he worked as a systems administrator for a large mortgage bank, managing servers and workstations, which is where he discovered his passion for automation and scripting. On his free time he enjoys camping with his wife and three kids.

Jan 24 2016
Jan 24
[embedded content]

Drupal Console is software which allows you to alter your Drupal installation through the command line. According to the official website, “The Drupal Console is a CLI tool to generate boilerplate code, interact and debug Drupal 8.” Unlike Drush, Drupal Console is specifically for Drupal 8, the latest major release.

Although Drupal Console and Drush share many capabilities such as clearing the cache, generating one-time login links, or un/installing modules/themes, one distinct functionality that comes out of the box with Drupal Console is that it can generate boilerplate code for modules, themes, controllers, forms, blocks, and much more.

Installing Drupal Console

Setting up Drupal Console is just as easy as executing the following commands found on the homepage of the website:

# Run this in your terminal to get the latest project version: curl https://drupalconsole.com/installer -L -o drupal.phar # Or if you don't have curl: php -r "readfile('https://drupalconsole.com/installer');" > drupal.phar # Accessing from anywhere on your system: mv drupal.phar /usr/local/bin/drupal # Apply executable permissions on the downloaded file: chmod +x /usr/local/bin/drupal # Run this in your terminal to get the latest project version:curl https://drupalconsole.com/installer -L -o drupal.phar# Or if you don't have curl:php -r "readfile('https://drupalconsole.com/installer');" > drupal.phar# Accessing from anywhere on your system:mv drupal.phar /usr/local/bin/drupal# Apply executable permissions on the downloaded file:chmod +x /usr/local/bin/drupal

To check if Drupal Console is working, execute drupal list. You should see a list of commands:

$ drupal list Drupal Console version 0.10.5 Usage: command [options] [arguments] Options: ... Available commands: ... module ... multisite ... site site:debug List all known local and remote sites. site:install Install a Drupal project site:new Create a new Drupal project theme ... yaml ... $ drupal listDrupal Console version 0.10.5command [options] [arguments]Available commands:site:debug List all known local and remote sites.site:install Install a Drupal projectsite:new Create a new Drupal project

Downloading Drupal

Let’s start from the beginning. We can actually get any version of Drupal by running drupal site:new:

$ drupal site:new Enter the directory name when downloading Drupal: > drupal Getting releases for Drupal Select a core release: [0 ] 8.0.2 [1 ] 8.0.1 [2 ] 8.0.0 [3 ] 8.0.0-rc4 [4 ] 8.0.0-rc3 [5 ] 8.0.0-rc2 [6 ] 8.0.0-rc1 [7 ] 8.0.0-beta16 [8 ] 8.0.0-beta15 [9 ] 8.0.0-beta14 [10] 8.0.0-beta13 [11] 8.0.0-beta12 [12] 8.0.0-beta11 [13] 8.0.0-beta10 [14] 8.0.0-beta9 > 0 Downloading drupal 8.0.2 [OK] Drupal 8.0.2 was downloaded in directory /home/ubuntu/workspace/drupal $ drupal site:newEnter the directory name when downloading Drupal:Getting releases for DrupalSelect a core release:[0 ] 8.0.2[1 ] 8.0.1[2 ] 8.0.0[3 ] 8.0.0-rc4[4 ] 8.0.0-rc3[5 ] 8.0.0-rc2[6 ] 8.0.0-rc1[7 ] 8.0.0-beta16[8 ] 8.0.0-beta15[9 ] 8.0.0-beta14[10] 8.0.0-beta13[11] 8.0.0-beta12[12] 8.0.0-beta11[13] 8.0.0-beta10[14] 8.0.0-beta9Downloading drupal 8.0.2[OK] Drupal 8.0.2 was downloaded in directory /home/ubuntu/workspace/drupal

Installing Drupal

Now, you might be thinking to go visit the running website to install Drupal. Wrong! Well, right you can do that. But, it is also possible via Drupal Console by simply running drupal site:install inside the drupal directory:

$ drupal site:install Select Drupal profile to be installed: [0] Minimal [1] Standard > 1 Select language for your Drupal installation [English]: > Drupal Database type: [0] MySQL, MariaDB, Percona Server, or equivalent [1] SQLite [2] PostgreSQL > 0 Database Host [127.0.0.1]: > Database Name: > drupal Database User: > {YOUR_DATABASE_USERNAME} Database Pass [ ]: > {YOUR_DATABASE_PASSWORD} Database Port [3306]: > Database Prefix [ ]: > Provide your Drupal site name [Drupal 8 Site Install]: > Drupal Console is Awesome! Provide your Drupal site mail [[email protected]]: > {YOUR_SITE_MAIL} Provide your Drupal administrator account name [admin]: > {YOUR_USER_NAME} Provide your Drupal administrator account mail [[email protected]]: > {YOUR_USER_MAIL} Provide your Drupal administrator account password: > {YOUR_USER_PASSWORD} Starting Drupal 8 install process [OK] Your Drupal 8 installation was completed successfully $ drupal site:installSelect Drupal profile to be installed:[0] Minimal[1] StandardSelect language for your Drupal installation [English]:Drupal Database type:[0] MySQL, MariaDB, Percona Server, or equivalent[1] SQLite[2] PostgreSQLDatabase Host [127.0.0.1]:Database Name:Database User:> {YOUR_DATABASE_USERNAME}Database Pass [ ]:> {YOUR_DATABASE_PASSWORD}Database Port [3306]:Database Prefix [ ]:Provide your Drupal site name [Drupal 8 Site Install]:> Drupal Console is Awesome!Provide your Drupal site mail [admin@example.com]:> {YOUR_SITE_MAIL}Provide your Drupal administrator account name [admin]:> {YOUR_USER_NAME}Provide your Drupal administrator account mail [admin@example.com]:> {YOUR_USER_MAIL}Provide your Drupal administrator account password:> {YOUR_USER_PASSWORD}Starting Drupal 8 install process[OK] Your Drupal 8 installation was completed successfully

Our new Drupal site should now be ready to be worked on:
Drupal Home

Activating Maintenance Mode

It is best to set a website in production to maintenance mode when being worked on. Be sure to log in first, so you can view the development of the site. Then you can turn on maintenance mode with drupal site:maintenance on:

$ drupal site:maintenance on Operating in maintenance mode on Rebuilding cache(s), wait a moment please. [OK] Done clearing cache(s). $ drupal site:maintenance onOperating in maintenance mode onRebuilding cache(s), wait a moment please.[OK] Done clearing cache(s).

Now, this is what regular users will see when visiting the website:

Drupal Maintenance

Creating a Hello World Module

Defining Module Parameters

Drupal Generate Module Directory Structure
First let’s generate code to define the module such as the .info.yml and the composer.json files by running drupal generate:module:

$ drupal generate:module // Welcome to the Drupal module generator Enter the new module name: > Hello World Enter the module machine name [hello_world]: > Enter the module Path [/modules/custom]: > Enter module description [My Awesome Module]: > Say Hello World Enter package name [Custom]: > Enter Drupal Core version [8.x]: > Do you want to generate a .module file (yes/no) [no]: > Define module as feature (yes/no) [no]: > Do you want to add a composer.json file to your module (yes/no) [yes]: > Would you like to add module dependencies (yes/no) [no]: > Do you confirm generation? (yes/no) [yes]: > Generated or updated files Site path: /home/ubuntu/workspace/drupal 1 - modules/custom/hello_world/hello_world.info.yml 2 - modules/custom/hello_world/composer.json $ drupal generate:module// Welcome to the Drupal module generatorEnter the new module name:> Hello WorldEnter the module machine name [hello_world]:Enter the module Path [/modules/custom]:Enter module description [My Awesome Module]:> Say Hello WorldEnter package name [Custom]:Enter Drupal Core version [8.x]:Do you want to generate a .module file (yes/no) [no]:Define module as feature (yes/no) [no]:Do you want to add a composer.json file to your module (yes/no) [yes]:Would you like to add module dependencies (yes/no) [no]:Do you confirm generation? (yes/no) [yes]:Generated or updated filesSite path: /home/ubuntu/workspace/drupal1 - modules/custom/hello_world/hello_world.info.yml2 - modules/custom/hello_world/composer.json

Installing the Module

Again, we can just enable to module using the command line using drupal module:install {PLUGIN_MACHINE_NAME}:

$ drupal module:install hello_world [OK] The following module(s) were installed successfully: hello_world Rebuilding cache(s), wait a moment please. [OK] Done clearing cache(s). $ drupal module:install hello_world[OK] The following module(s) were installed successfully: hello_worldRebuilding cache(s), wait a moment please.[OK] Done clearing cache(s).

Generating the Controller

Now, we need to generate a controller that will show the “Hello World” page. Do this using drupal generate:controller:

$ drupal generate:controller // Welcome to the Drupal Controller generator Enter the module name [hello_world]: > Enter the Controller class name [DefaultController]: > HelloWorldController Enter the Controller method title (leave empty and press enter when done) [ ]: > Hello World Enter the action method name [hello]: > helloWorld Enter the route path [hello_world/hello/{name}]: > /hello/world Enter the Controller method title (leave empty and press enter when done) [ ]: > Do you want to generate a unit test class (yes/no) [yes]: > Do you want to load services from the container (yes/no) [no]: > Do you confirm generation? (yes/no) [yes]: > Generated or updated files Site path: /home/ubuntu/workspace/drupal 1 - modules/custom/hello_world/src/Controller/HelloWorldController.php 2 - modules/custom/hello_world/hello_world.routing.yml 3 - modules/custom/hello_world/Tests/Controller/HelloWorldControllerTest.php Rebuilding routes, wait a moment please [OK] Done rebuilding route(s). $ drupal generate:controller// Welcome to the Drupal Controller generatorEnter the module name [hello_world]:Enter the Controller class name [DefaultController]:> HelloWorldControllerEnter the Controller method title (leave empty and press enter when done) [ ]:> Hello WorldEnter the action method name [hello]:> helloWorldEnter the route path [hello_world/hello/{name}]:> /hello/worldEnter the Controller method title (leave empty and press enter when done) [ ]:Do you want to generate a unit test class (yes/no) [yes]:Do you want to load services from the container (yes/no) [no]:Do you confirm generation? (yes/no) [yes]:Generated or updated filesSite path: /home/ubuntu/workspace/drupal1 - modules/custom/hello_world/src/Controller/HelloWorldController.php2 - modules/custom/hello_world/hello_world.routing.yml3 - modules/custom/hello_world/Tests/Controller/HelloWorldControllerTest.phpRebuilding routes, wait a moment please[OK] Done rebuilding route(s).

Drupal Hello World Directory Structure

This will automatically generate our controller file, routing file, and even our Test file.

Finished Module

Now, if we visit http://www.mydrupalwebsite.com/hello/world, we should see:

Drupal Hello World 1

This way of generating modules is much quicker than normal. You can compare it to normally creating a Hello World module.

Creating a Block

We can also make a block to go along with this module and have it hold a configuration value. This can be done by running drupal generate:plugin:block:

$ drupal generate:plugin:block // Welcome to the Drupal Plugin Block generator Enter the module name [hello_world]: > Plugin class name [DefaultBlock]: > Plugin label [Default block]: > Plugin id [default_block]: > Theme region to render Plugin Block [ ]: > Do you want to load services from the container (yes/no) [no]: > You can add input fields to create special configurations in the block. This is optional, press enter to continue Do you want to generate a form structure? (yes/no) [yes]: > Type [ ]: > textfield Input label: > Content Input machine name [content]: > Maximum amount of characters [64]: > Width of the textfield (in characters) [64]: > Description [ ]: > Content to Display Default value [ ]: > Weight for input item [0]: > Type [ ]: > Do you confirm generation? (yes/no) [yes]: > Generated or updated files Site path: /home/ubuntu/workspace/drupal 1 - modules/custom/hello_world/src/Plugin/Block/DefaultBlock.php Rebuilding cache(s), wait a moment please. [OK] Done clearing cache(s). $ drupal generate:plugin:block// Welcome to the Drupal Plugin Block generatorEnter the module name [hello_world]:Plugin class name [DefaultBlock]:Plugin label [Default block]:Plugin id [default_block]:Theme region to render Plugin Block [ ]:Do you want to load services from the container (yes/no) [no]:You can add input fields to create special configurations in the block.This is optional, press enter to continueDo you want to generate a form structure? (yes/no) [yes]:> textfieldInput label:Input machine name [content]:Maximum amount of characters [64]:Width of the textfield (in characters) [64]:Description [ ]:> Content to DisplayDefault value [ ]:Weight for input item [0]:Do you confirm generation? (yes/no) [yes]:Generated or updated filesSite path: /home/ubuntu/workspace/drupal1 - modules/custom/hello_world/src/Plugin/Block/DefaultBlock.phpRebuilding cache(s), wait a moment please.[OK] Done clearing cache(s).

Drupal Plugin Block Directory Structure

If we now go to Admin > Structure > Block layout > Sidebar second > Place Block, we can see our newly generated block there and add it to the sidebar:

Drupal Place Block
Drupal Configure Block
Drupal Default Block

Generating Random Nodes

If we are just testing our website for development and need some content, nodes can be easily generated using drupal create:nodes:

$ drupal create:nodes // Welcome to the Drupal nodes generator Select content type(s) to be used on node creation: [0] Article [1] Basic page > 0 Enter how many nodes would you like to generate [10]: > Enter the maximum number of words in titles [5]: > How far back in time should the nodes be dated?: [0] N | Now [1] H | 1 hour ago [2] D | 1 day ago [3] W | 1 week ago [4] M | 1 month ago [5] Y | 1 year ago > 5 --------- -------------- ---------------------------- --------------------- Node Id Content type Title Created Time --------- -------------- ---------------------------- --------------------- 1 Article Capto Eros Haero Uxor 2016-01-18 03:23:09 2 Article Nunc Voco 2015-02-08 11:23:37 3 Article Abdo Eu Pala Plaga 2015-10-16 02:26:27 4 Article Commoveo Erat Iustum 2015-09-15 05:52:22 5 Article Olim 2015-08-13 10:50:27 6 Article Comis Damnum Ille Vulpes 2015-12-11 03:19:55 7 Article Gravis Probo 2015-04-16 07:38:44 8 Article Nobis Quidem Torqueo Zelus 2015-10-25 06:47:05 9 Article Commoveo Duis Metuo Quidne 2015-05-30 10:21:16 10 Article Illum Mos Obruo Tamen 2015-04-22 01:47:57 --------- -------------- ---------------------------- --------------------- [OK] Created 10 nodes successfully $ drupal create:nodes// Welcome to the Drupal nodes generatorSelect content type(s) to be used on node creation:[0] Article[1] Basic pageEnter how many nodes would you like to generate [10]:Enter the maximum number of words in titles [5]:How far back in time should the nodes be dated?:[0] N | Now[1] H | 1 hour ago[2] D | 1 day ago[3] W | 1 week ago[4] M | 1 month ago[5] Y | 1 year ago--------- -------------- ---------------------------- ---------------------Node Id Content type Title Created Time--------- -------------- ---------------------------- ---------------------1 Article Capto Eros Haero Uxor 2016-01-18 03:23:092 Article Nunc Voco 2015-02-08 11:23:373 Article Abdo Eu Pala Plaga 2015-10-16 02:26:274 Article Commoveo Erat Iustum 2015-09-15 05:52:225 Article Olim 2015-08-13 10:50:276 Article Comis Damnum Ille Vulpes 2015-12-11 03:19:557 Article Gravis Probo 2015-04-16 07:38:448 Article Nobis Quidem Torqueo Zelus 2015-10-25 06:47:059 Article Commoveo Duis Metuo Quidne 2015-05-30 10:21:1610 Article Illum Mos Obruo Tamen 2015-04-22 01:47:57--------- -------------- ---------------------------- ---------------------[OK] Created 10 nodes successfully

Now, you can see some of these articles posted to the front page with images:

Drupal Front Page

Generating a Theme

Apart from generating modules and content, we can also generate themes. Let’s generate a basic theme that extends the classy base theme by running drupal generate:theme:

$ drupal generate:theme // Welcome to the Drupal theme generator Enter the new theme name []: > New Theme Enter the module machine name [new_theme]: > Enter the theme Path [/themes/custom]: > Enter theme description [My Awesome theme]: > Plain Theme Enter package name [Other]: > Enter Drupal Core version [8.x]: > Base theme (i.e. classy, stable) [bartik]: > classy Enter the global styling library name [global-styling]: > Do you want to generate the theme regions (yes/no) [yes]: > Enter region name [Content]: > Enter region machine name [content]: > Do you want add another region (yes/no) [yes]: > no Do you want to generate the theme breakpoints (yes/no) [yes]: > Enter breakpoint name [narrow]: > Enter breakpoint label [narrow]: > Enter breakpoint media query [all and (min-width: 560px) and (max-width: 850px)]: > Enter breakpoint weight [1]: > Enter breakpoint multipliers [1x]: > Do you want to add another breakpoint (yes/no) [yes]: > no Do you confirm generation? (yes/no) [yes]: > Generated or updated files Site path: /home/ubuntu/workspace/drupal 1 - themes/custom/new_theme/new_theme.info.yml 2 - themes/custom/new_theme/new_theme.theme 3 - themes/custom/new_theme/new_theme.breakpoints.yml $ drupal generate:theme// Welcome to the Drupal theme generatorEnter the new theme name []:> New ThemeEnter the module machine name [new_theme]:Enter the theme Path [/themes/custom]:Enter theme description [My Awesome theme]:> Plain ThemeEnter package name [Other]:Enter Drupal Core version [8.x]:Base theme (i.e. classy, stable) [bartik]:Enter the global styling library name [global-styling]:Do you want to generate the theme regions (yes/no) [yes]:Enter region name [Content]:Enter region machine name [content]:Do you want add another region (yes/no) [yes]:Do you want to generate the theme breakpoints (yes/no) [yes]:Enter breakpoint name [narrow]:Enter breakpoint label [narrow]:Enter breakpoint media query [all and (min-width: 560px) and (max-width: 850px)]:Enter breakpoint weight [1]:Enter breakpoint multipliers [1x]:Do you want to add another breakpoint (yes/no) [yes]:Do you confirm generation? (yes/no) [yes]:Generated or updated filesSite path: /home/ubuntu/workspace/drupal1 - themes/custom/new_theme/new_theme.info.yml2 - themes/custom/new_theme/new_theme.theme3 - themes/custom/new_theme/new_theme.breakpoints.yml

Drupal Theme Directory Structure

Three new files should have been generated in the themes folder. You should now be able to install the theme by running drupal theme:install {THEME_MACHINE_NAME}:

$ drupal theme:install new_theme The New Theme theme has been installed successfully Rebuilding cache(s), wait a moment please. [OK] Done clearing cache(s). $ drupal theme:install new_themeThe New Theme theme has been installed successfullyRebuilding cache(s), wait a moment please.[OK] Done clearing cache(s).

The theme still needs to be set as default before we can see it in action. However, it will just be a plain style-less theme because it just extends classy:

Drupal New Theme

Deactivating Maintenance Mode

As we are done with our development, stop maintenance mode by running drupal site:maintenance off:

$ drupal site:maintenance off Operating in maintenance mode off Rebuilding cache(s), wait a moment please. [OK] Done clearing cache(s). $ drupal site:maintenance offOperating in maintenance mode offRebuilding cache(s), wait a moment please.[OK] Done clearing cache(s).

This post just covered the surface of what all Drupal Console can do. It can do so much more such as generating Forms, Permissions, and even most of the features from Drush. Remember to explore all the commands by running drupal list. It is a tool every Drupal developer should be taking advantage of. You can learn more of the commands and shortcuts on the Drupal Console Documentation.

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Jan 20 2016
Jan 20

This post is part 4 in the series "Hashing out a docker workflow". For background, checkout my previous posts.

My previous posts talked about getting your local environment setup using the Drupal Docker image with Vagrant. It's now time to bake a Docker image with our custom application code within the container, so that we can deploy containers implementing the immutable server pattern. One of the main reasons we starting venturing down the Docker path was to achieve deployable fully baked containers that are ready to run in whatever environment you put them in, similar to what we've done in the past with Packer, as I've mentioned in a previous post.

Review

The instructions in this post are assumming you followed my previous post to get a Drupal environment setup with the custom "myprofile" profile. In that post we brought up a Drupal environment by just referencing the already built Drupal image on DockerHub. We are going to use that same Docker image, and add our custom application to that.

All the code that I'm going to show below can be found in this repo on Github.

Putting the custom code into the container

We need to create our own image, create a Dockerfile in our project that extends the Drupal image that we are pulling down.

Create a file called Dockerfile in the root of your project that looks like the following:

FROM drupal:7.41

ADD drupal/profiles/myprofile /var/www/html/profiles/myprofile

We are basically using everything from the Drupal image, and adding our installation profile to the profiles directory of the document root.

This is a very simplistic approach, typically there are more steps than just copying files over. In more complex scenarios, you will likely run some sort of build within the Dockerfile as well, such as Gulp, Composer, or Drush Make.

Setting up Jenkins

We now need to setup a Jenkins server that will checkout our code and run docker build and docker push. Let's setup a local jenkins container on our Docker host to do this.

Open up the main Vagrantfile in the project root and add another container to the file like the following:





Vagrant.configure(2) do |config|

config.vm.define "jenkins" do |v|
v.vm.provider "docker" do |d|
d.vagrant_vagrantfile = "./host/Vagrantfile"
d.build_dir = "./Dockerfiles/jenkins"
d.create_args = ['--privileged']
d.remains_running = true
d.ports = ["8080:8080"]
d.name = "jenkins-container"
end
end

config.vm.define "drupal" do |v|
config.vm.provider "docker" do |docker|
docker.vagrant_vagrantfile = "host/Vagrantfile"
docker.image = "drupal"
docker.create_args = ['--volume="/srv/myprofile:/var/www/html/profiles/myprofile"']
docker.ports = ['80:80']
docker.name = 'drupal-container'
end
end
end

Two things to notice from the jenkins container definition, 1) The Dockerfile for this container is in the Dockerfiles/jenkins directory, and 2) we are passing the --privileged argument when the container is run so that our container has all the capabilities of the docker host. We need special access to be able to run Docker within Docker.

Lets create the Dockerfile:

$ mkdir -p Dockerfiles/jenkins
$ cd !$
$ touch Dockerfile

Now open up that Dockerfile and install Docker onto this Jenkins container:

FROM jenkins:1.625.2

USER root


RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D


RUN echo "deb http://apt.dockerproject.org/repo debian-jessie main" > /etc/apt/sources.list.d/docker.list

VOLUME /var/lib/docker

RUN apt-get update && \
  apt-get -y install \
    docker-engine

ADD ./dockerjenkins.sh /usr/local/bin/dockerjenkins.sh
RUN chmod +x /usr/local/bin/dockerjenkins.sh

ENTRYPOINT ["/bin/tini", "--", "/usr/local/bin/dockerjenkins.sh" ]

We are using a little script that is found in The Docker Book as our entry point to start the docker daemon, as well as Jenkins. It also does some stuff on the filesystem to ensure cgroups are mounted correctly. If you want to read more about running Docker in Docker, go check out this article

Boot up the new container

Before we boot this container up, edit your host Vagrantfile and setup the port forward so that 8080 points to 8080:





Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.hostname = "docker-host"
config.vm.provision "docker"
config.vm.network :forwarded_port, guest: 80, host: 4567
config.vm.network :forwarded_port, guest: 8080, host: 8080
config.vm.synced_folder '../drupal/profiles/myprofile', '/srv/myprofile', type: 'rsync'
end

Now bring up the new container:

$ vagrant up jenkins

or if you've already brought it up once before, you may just need to run reload:

\$ vagrant reload jenkins

You should now be able to hit Jenkins at the URL http://localhost:8080

Jenkins Dashboard

Install the git plugins for Jenkins

Now that you have Jenkins up and running, we need to install the git plugins. Click on the "Manage Jenkins" link in the left navigation, then click "Manage Plugins" in the list given to you, and then click on the "Available" Tab. Filter the list with the phrase "git client" in the filter box. Check the two boxes to install plugins, then hit "Download now and install after restart".

Jenkins Plugin Install

On the following screen, check the box to Restart Jenkins when installation is complete.

Setup the Jenkins job

It's time to setup Jenkins. If you've never setup a Jenkins job, here is a quick crash course.

  1. Click the New Item link in the left navigation. Name your build job, and choose Freestyle project. Click Ok. New Build Job
  2. Configure the git repo. We are going to configure Jenkins to pull code directly from your repository and build the Docker image from that. Add the git Repo
  3. Add the build steps. Scroll down toward the bottom of the screen and click the arrow next to Add build step and choose Execute Shell. We are going to add three build steps as shown below. First we build the Docker image with docker build -t="tomfriedhof/docker_blog_post" . (notice the trailing dot) and give it a name with the -t parameter, then we login to DockerHub, and finally push the newly created image that was created to DockerHub. Jenkins Build Steps
  4. Hit Save, then on the next screen hit the button that says Build Now

If everything went as planned, you should have a new Docker image posted on DockerHub: https://hub.docker.com/r/tomfriedhof/dockerblogpost/

Wrapping it up

There you have it, we now have an automated build that will automatically create and push Docker images to DockerHub. You can add on to this Jenkins job so that it polls your Github Repository so that it automatically runs this build anytime something changes in the tracking repo.

As another option, if you don't want to go through all the trouble of setting up your own Jenkins server just to do what I just showed you, DockerHub can do this for you. Go checkout their article on how to setup automated builds with Docker.

Now that we have a baked container with our application code within it, the next step is to deploy the container. That is the next post in this series. Stay tuned!

Jan 09 2016
Jan 09
[embedded content]

Drupal 8 came out with many new features and updates at the end of 2015. As Drupal 8 is object oriented and enforces PSR-4 standards, the way you make modules has significantly changed. However, this change makes modules much more organized to fit today’s coding practices. I will be demonstrating how to create a simple “Hello World!” module in Drupal 8.

Getting Started

I will be assuming you already have a working Drupal 8 website.

Creating The Module Directory

Drupal Module Directory Structure 1Drupal 8 has a much cleaner folder structure than before. All the modules will go in the module folder. Create a hello_world folder here. This will serve as the machine name for the module and will contain all the necessary files for our module to function.

Defining Parameters

Module Information

First, parameters such as title and description need to be set. These keys will be defined in the file hello_world.info.yml. Unlike Drupal 7, Drupal 8 uses YAML (YAML Ain’t Markup Language) for these types of files. Create this file and enter these definitions:

name: Hello World type: module description: Say Hello World package: Custom core: 8.x name: Hello Worldtype: moduledescription: Say Hello Worldpackage: Custom
  • name: Hello World
    • This is the title of our module shown on the extend page.
  • type: module
    • This is to tell Drupal what we are making is a module.
  • description: Say Hello World
    • This is a description shown alongside the title on the extend page.
  • package: Custom
    • This is what category our module will be listed under on the extend page.
  • core: 8.x
    • This tells Drupal our module is compatible with Drupal 8.x core.

You will now be able to see the module listed under the Custom package category:

Drupal Module List

Let’s enable this module:

Drupal Module Enabled

Module Routes

Next, we will need to tell Drupal where the module can be accessed. This is done by creating a route for our module. Create the file hello_world.routing.yml and enter the following parameters:

hello_world: path: /hello/world defaults: _controller: Drupal\hello_world\Controller\HelloWorldController::hello requirements: _permission: 'access content' hello_world:    path: /hello/world    defaults:        _controller: Drupal\hello_world\Controller\HelloWorldController::hello    requirements:        _permission: 'access content'
  • path: /hello/world
    • This tells Drupal what path will be used to access our module.
  • _controller: Drupal\hello_world\Controller\HelloWorldController::hello
    • This is the method Drupal will call to process a request to our path.
  • _permission: 'access content'
    • This is to ensure only users who can access content will be able to see our Hello World page.

Quotes are not required around the values, however, you can include them to be safe.

Drupal Module Directory Structure 2

Your current directory structure should now look like this:

Coding the Module

After defining all the parameters for the module, we still need to add functionality to our module. We need to write code that will live in a “Controller”. Since Drupal 8 follows PSR-4 standards, there is a specific directory structure to follow. All code will exist inside the src folder. After creating this, make a Controller folder. This is where the PHP Class will be made. Here, create HelloWorldController.php and define the as so:

<?php class HelloWorldController { } class HelloWorldController {

Any Drupal 8 class will also need to be namespaced. This is just to make sure the right classes are referenced from outside if some classes have the same name and it makes it more organized. Add this above the class definition:

<?php namespace Drupal\hello_world\Controller; class HelloWorldController { } namespace Drupal\hello_world\Controller;class HelloWorldController {

Remember in the routing file we told Drupal to call the hello method in the class Drupal\hello_world\Controller\HelloWorldController. Let’s define this in our class:

<?php namespace Drupal\hello_world\Controller; class HelloWorldController { public function hello() { } } namespace Drupal\hello_world\Controller;class HelloWorldController {    public function hello() {

Since Drupal 8 is based on the Symfony Framework, a new Symfony\Component\HttpFoundation\Response object can be returned. However, since this is Drupal, you can also return a render array which Drupal will process and automatically add theming. For our simple page, we can return an array with keys #title and #markup:

<?php namespace Drupal\hello_world\Controller; class HelloWorldController { public function hello() { return array( '#title' => 'Hello World!', '#markup' => 'Here is some content.', ); } } namespace Drupal\hello_world\Controller;class HelloWorldController {    public function hello() {        return array(                '#title' => 'Hello World!',                '#markup' => 'Here is some content.',            );

Finishing Up

Before we visit our custom page, we need to clear the cache, specifically the router cache. Otherwise, you will see a 404 Not Found page. You can clear the cache via Drupal Console:

Drupal Router Rebuild

Or through the Admin Control Panel:

Drupal Clear All Caches

Now, if we visit http://www.mydrupalwebsite.com/hello/world, we can see our custom page:

Drupal Hello World

The Full Module

Drupal Module Directory Structure FinalHere is the final directory structure and code of our Hello World module:

*/

That’s it! Creating Drupal modules is as easy as that! Now that you
know how to define module parameters and set up your own Controller to handle specific route requests, you can move on to create more complex Drupal modules using other components such as Forms and implementing Services.

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Dec 26 2015
Dec 26

It has been a long time coming. With over 200 new features developed during the last 5 years by more than 2,000 contributors, the most anticipated version of Drupal was finally announced November 19th, 2015. This new release of Drupal contains spectacular features worth upgrading for than any previous release. The improvements evident in Drupal 8 justify that this is one of the best and largest updates in Drupal history.

User Interface and Experience

User

Posts and Pages

Drupal 8 makes use of a library called CKEditor in the admin panel. This library allows you to edit new posts and pages in a rich WYSIWYG editor unlike before. Also, thanks to “Quick Edit”, content is now able to be changed right in the front-end when you are viewing the post.

Mobile-First

As more and more websites are being accessed from mobile phones, it is necessary that websites are first made to support mobile and then desktop. This is exactly what Drupal 8 is doing. Drupal 8 is responsive both in the front-end and admin panel. Images and the site design will rearrange depending on device width. All themes including the defaults, Bartik and Seven, work seamlessly on mobile devices, tablets, and desktops.

Multilingual

Internationalization is now built into Drupal. This means that no contributed modules are needed for translation. All the modules are included in a fresh install of Drupal 8. Everything is translatable such as Content, Blocks, Menus, Views, Comments, Feeds and more. You can choose which language to use on your Drupal website on the first page of the installation process and the translations will be automatically downloaded.

Developers

Code

Front-end

Drupal 8 has switched its templating engine to Twig from PHPTemplate which has a more friendly syntax for designers. HTML5 Forms are also included which means mobile users can be displayed a variety of keyboards for different types of input. Some of the new fields include Date, Email, Link, Reference, and Telephone.

Object Oriented PHP

Drupal 8 has adopted the latest best practices for PHP. It uses PHP 5.4+, Classes and Interfaces, Namespaces, Traits, Dependency Injection, and conforms to the PSR-4 standards for file paths to autoload classes. Drupal 8 is built on top of several proven Symfony 2 modules and utilizes several other libraries such as PHPUnit, Composer, and jQuery. To create modules, themes, blocks and more you will now need to use classes and new info files in YAML.

Configuration Management

There is now a central location to store and retrieve saved information and configurations. This will improve organization and simplify creating modules for Drupal 8. These configurations can be easily exported and imported into another Drupal 8 website.

Anyone running Drupal 6 or 7 is encouraged to update to Drupal 8, especially if you are running Drupal 6 because this will no longer be supported. Since no more updates will be provided, your website will be vulnerable to security risks in the future. Regardless of security risks, these spectacular features should be enough to convince you to upgrade to Drupal 8!

For more information about Drupal 8, Click Here. This post was created during Google Code-in 2015, an open source competition for high school students.

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Dec 15 2015
Dec 15

Tom Friedhof

Senior Software Engineer

Tom has been designing and developing for the web since 2002 and got involved with Drupal in 2006. Previously he worked as a systems administrator for a large mortgage bank, managing servers and workstations, which is where he discovered his passion for automation and scripting. On his free time he enjoys camping with his wife and three kids.

Dec 02 2015
Dec 02

Now that the release of Drupal 8 is finally here, it is time to adapt our Drupal 7 build process to Drupal 8, while utilizing Docker. This post will take you through how we construct sites on Drupal 8 using dependency managers on top of Docker with Vagrant.

Keep a clean upstream repo

Over the past 3 or 4 years developing websites has changed dramatically with the increasing popularity of dependency management such as Composer, Bundler, npm, Bower, etc... amongst other tools. Drupal even has it's own system that can handle dependencies called Drush, albiet it is more than just a dependency manager for Drupal.

With all of these tools at our disposal, it makes it very easy to include code from other projects in our application while not storing any of that code in the application code repository. This concept dramatically changes how you would typically maintain a Drupal site, since the typical way to manage a Drupal codebase is to have the entire Drupal Docroot, including all dependencies, in the application code repository. Having everything in the docroot is fine, but you gain so much more power using dependency managers. You also lighten up the actual application codebase when you utilize dependency managers, because your repo only contains code that you wrote. There are tons of advantages to building applications this way, but I have digressed, this post is about how we utilize these tools to build Drupal sites, not an exhaustive list of why this is a good idea. Leave a comment if you want to discuss the advantages / disadvantages of this approach.

Application Code Repository

We've got a lot going on in this repository. We won't dive too deep into the weeds looking at every single file, but I will give a high level overview of how things are put together.

Installation Automation (begin with the end in mind)

The simplicity in this process is that when a new developer needs to get a local development environment for this project, they only have to execute two commands:

$ vagrant up --no-parallel
$ make install

Within minutes a new development environment is constructed with Virtualbox and Docker on the developers machine, so that they can immediately start contributing to the project. The first command boots up 3 Docker containers -- a webserver, mysql server, and jenkins server. The second command invokes Drush to build the document root within the webserver container and then installs Drupal.

We also utilize one more command to keep running within a seperate terminal window, to keep files synced from our host machine to the Drupal 8 container.

$ vagrant rsync-auto drupal8

Breaking down the two installation commands

vagrant up --no-parallel

If you've read any of my previous posts, I'm a fan of using Vagrant with Docker. I won't go into detail about how the environment is getting set up. You can read my previous posts on how we used Docker with Vagrant. For completeness, here is the Vagrantfile and Dockerfile that vagrant up reads to setup the environment.

Vagrantfile





require 'fileutils'

MYSQL_ROOT_PASSWORD="root"

unless File.exists?("keys")
Dir.mkdir("keys")
ssh_pub_key = File.readlines("#{Dir.home}/.ssh/id_rsa.pub").first.strip
File.open("keys/id_rsa.pub", 'w') { |file| file.write(ssh_pub_key) }
end

unless File.exists?("Dockerfiles/jenkins/keys")
Dir.mkdir("Dockerfiles/jenkins/keys")
FileUtils.copy("#{Dir.home}/.ssh/id_rsa", "Dockerfiles/jenkins/keys/id_rsa")
end

Vagrant.configure("2") do |config|

config.vm.define "mysql" do |v|

    v.vm.provider "docker" do |d|
      d.vagrant_machine = "apa-dockerhost"
      d.vagrant_vagrantfile = "./host/Vagrantfile"
      d.image = "mysql:5.7.9"
      d.env = { :MYSQL_ROOT_PASSWORD => MYSQL_ROOT_PASSWORD }
      d.name = "mysql-container"
      d.remains_running = true
      d.ports = [
        "3306:3306"
      ]
    end

end

config.vm.define "jenkins" do |v|

    v.vm.synced_folder ".", "/srv", type: "rsync",
        rsync__exclude: get_ignored_files(),
        rsync__args: ["--verbose", "--archive", "--delete", "--copy-links"]

    v.vm.provider "docker" do |d|
      d.vagrant_machine = "apa-dockerhost"
      d.vagrant_vagrantfile = "./host/Vagrantfile"
      d.build_dir = "./Dockerfiles/jenkins"
      d.name = "jenkins-container"
      
      d.volumes = [
          "/home/rancher/.composer:/root/.composer",
          "/home/rancher/.drush:/root/.drush"
      ]
      d.remains_running = true
      d.ports = [
          "8080:8080"
      ]
    end

end

config.vm.define "drupal8" do |v|

    v.vm.synced_folder ".", "/srv/app", type: "rsync",
      rsync__exclude: get_ignored_files(),
      rsync__args: ["--verbose", "--archive", "--delete", "--copy-links"],
      rsync__chown: false

    v.vm.provider "docker" do |d|
      d.vagrant_machine = "apa-dockerhost"
      d.vagrant_vagrantfile = "./host/Vagrantfile"
      d.build_dir = "."
      d.name = "drupal8-container"
      d.remains_running = true
      
      d.volumes = [
        "/home/rancher/.composer:/root/.composer",
        "/home/rancher/.drush:/root/.drush"
      ]
      d.ports = [
        "80:80",
        "2222:22"
      ]
      d.link("mysql-container:mysql")
    end

end

end

def get_ignored_files()
ignore_file = ".rsyncignore"
ignore_array = []

if File.exists? ignore_file and File.readable? ignore_file
File.read(ignore_file).each_line do |line|
ignore_array << line.chomp
end
end

ignore_array
end

One of the cool things to point out that we are doing in this Vagrantfile is setting up a VOLUME for the composer and drush cache that should persist beyond the life of the container. When our application container is rebuilt we don't want to download 100MB of composer dependencies every time. By utilizing a Docker VOLUME, that folder is mounted to the actual Docker host.

Dockerfile (drupal8-container)

FROM ubuntu:trusty


ENV PROJECT_ROOT /srv/app
ENV DOCUMENT_ROOT /var/www/html
ENV DRUPAL_PROFILE=apa_profile


RUN apt-get update
RUN apt-get install -y \
	vim \
	git \
	apache2 \
	php-apc \
	php5-fpm \
	php5-cli \
	php5-mysql \
	php5-gd \
	php5-curl \
	libapache2-mod-php5 \
	curl \
	mysql-client \
	openssh-server \
	phpmyadmin \
	wget \
	unzip \
	supervisor
RUN apt-get clean


RUN curl -sS https://getcomposer.org/installer | php
RUN mv composer.phar /usr/local/bin/composer


RUN mkdir /root/.ssh && chmod 700 /root/.ssh && touch /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys
RUN echo 'root:root' | chpasswd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN mkdir /var/run/sshd && chmod 0755 /var/run/sshd
RUN mkdir -p /root/.ssh
COPY keys/id_rsa.pub /root/.ssh/authorized_keys
RUN chmod 600 /root/.ssh/authorized_keys
RUN sed '[email protected]\s*required\s*[email protected] optional [email protected]' -i /etc/pam.d/sshd


RUN composer global require drush/drush:8.0.0-rc3
RUN ln -nsf /root/.composer/vendor/bin/drush /usr/local/bin/drush


RUN mv /root/.composer /tmp/


RUN sed -i 's/display_errors = Off/display_errors = On/' /etc/php5/apache2/php.ini
RUN sed -i 's/display_errors = Off/display_errors = On/' /etc/php5/cli/php.ini


RUN sed -i 's/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
RUN a2enmod rewrite


RUN echo '[program:apache2]\ncommand=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2 -DFOREGROUND"\nautorestart=true\n\n' >> /etc/supervisor/supervisord.conf
RUN echo '[program:sshd]\ncommand=/usr/sbin/sshd -D\n\n' >> /etc/supervisor/supervisord.conf





WORKDIR $PROJECT_ROOT
EXPOSE 80 22
CMD exec supervisord -n

We have xdebug commented out in the Dockerfile, but it can easily be uncommented if you need to step through code. Simply uncomment the two RUN commands and run vagrant reload drupal8

make install

We utilize a Makefile in all of our projects whether it be Drupal, nodejs, or Laravel. This is so that we have a similar way to install applications, regardless of the underlying technology that is being executed. In this case make install is executing a drush command. Below is the contents of our Makefile for this project:

all: init install

init:
vagrant up --no-parallel

install:
bin/drush @dev install.sh

rebuild:
bin/drush @dev rebuild.sh

clean:
vagrant destroy drupal8
vagrant destroy mysql

mnt:
sshfs -C -p 2222 [email protected]:/var/www/html docroot

What this commmand does is ssh into the drupal8-container, utilizing drush aliases and drush shell aliases.

install.sh

The make install command executes a file, within the drupal8-container, that looks like this:

#!/usr/bin/env bash

echo "Moving the contents of composer cache into place..."
mv /tmp/.composer/* /root/.composer/

PROJECT_ROOT=$PROJECT_ROOT DOCUMENT_ROOT=$DOCUMENT_ROOT $PROJECT_ROOT/bin/rebuild.sh

echo "Installing Drupal..."
cd $DOCUMENT_ROOT && drush si $DRUPAL_PROFILE --account-pass=admin -y
chgrp -R www-data sites/default/files
rm -rf ~/.drush/files && cp -R sites/default/files ~/.drush/

echo "Importing config from sync directory"
drush cim -y

You can see on line 6 of install.sh file that it executes a rebuild.sh file to actually build the Drupal document root utilizing Drush Make. The reason for separating the build from the install is so that you can run make rebuild without completely reinstalling the Drupal database. After the document root is built, the drush site-install apa_profile command is run to actually install the site. Notice that we are utilizing Installation Profiles for Drupal.

We utilize installation profiles so that we can define modules available for the site, as well as specify default configuration to be installed with the site.

We work hard to achieve the ability to have Drupal install with all the necessary configuration in place out of the gate. We don't want to be passing around a database to get up and running with a new site.

We utilize the Devel Generate module to create the initial content for sites while developing.

rebuild.sh

The rebuild.sh file is responsible for building the Drupal docroot:

#!/usr/bin/env bash

if [ -d "$DOCUMENT_ROOT/sites/default/files" ]
then
echo "Moving files to ~/.drush/..."
mv \$DOCUMENT_ROOT/sites/default/files /root/.drush/
fi

echo "Deleting Drupal and rebuilding..."
rm -rf \$DOCUMENT_ROOT

echo "Downloading contributed modules..."
drush make -y $PROJECT_ROOT/drupal/make/dev.make $DOCUMENT_ROOT

echo "Symlink profile..."
ln -nsf $PROJECT_ROOT/drupal/profiles/apa_profile $DOCUMENT_ROOT/profiles/apa_profile

echo "Downloading Composer Dependencies..."
cd $DOCUMENT_ROOT && php $DOCUMENT_ROOT/modules/contrib/composer_manager/scripts/init.php && composer drupal-update

echo "Moving settings.php file to $DOCUMENT_ROOT/sites/default/..."
rm -f $DOCUMENT_ROOT/sites/default/settings\*
cp $PROJECT_ROOT/drupal/config/settings.php $DOCUMENT_ROOT/sites/default/
cp $PROJECT_ROOT/drupal/config/settings.local.php $DOCUMENT_ROOT/sites/default/
ln -nsf $PROJECT_ROOT/drupal/config/sync $DOCUMENT_ROOT/sites/default/config
chown -R www-data \$PROJECT_ROOT/drupal/config/sync

if [ -d "/root/.drush/files" ]
then
cp -Rf /root/.drush/files $DOCUMENT_ROOT/sites/default/
    chmod -R g+w $DOCUMENT_ROOT/sites/default/files
chgrp -R www-data sites/default/files
fi

This file essentially downloads Drupal using the dev.make drush make file. It then runs composer drupal-update to download any composer dependencies in any of the modules. We use the composer manager module to help with composer dependencies within the Drupal application.

Running the drush make dev.make includes two other Drush Make files, apa-cms.make (the application make file) and drupal-core.make. Only dev dependencies should go in dev.make. Application dependencies go into apa-cms.make. Any core patches that need to be applied go into drupal-core.make.

Our Jenkins server builds the prod.make file, instead of dev.make. Any production specific modules would go in prod.make file.

Our make files for this project look like this so far:

dev.make

core: "8.x"

api: 2

defaults:
  projects:
    subdir: "contrib"

includes:
  - "apa-cms.make"

projects:
  devel:
    version: "1.x-dev"

apa-cms.make

core: "8.x"

api: 2

defaults:
projects:
subdir: "contrib"

includes:

- drupal-core.make

projects:
address:
version: "1.0-beta2"

composer_manager:
version: "1.0-rc1"

config_update:
version: "1.x-dev"

ctools:
version: "3.0-alpha17"

draggableviews:
version: "1.x-dev"

ds:
version: "2.0"

features:
version: "3.0-alpha4"

field_collection:
version: "1.x-dev"

field_group:
version: "1.0-rc3"

juicebox:
version: "2.0-beta1"

layout_plugin:
version: "1.0-alpha19"

libraries:
version: "3.x-dev"

menu_link_attributes:
version: "1.0-beta1"

page_manager:
version: "1.0-alpha19"

pathauto:
type: "module"
download:
branch: "8.x-1.x"
type: "git"
url: "http://github.com/md-systems/pathauto.git"

panels:
version: "3.0-alpha19"

token:
version: "1.x-dev"

zurb_foundation:
version: "5.0-beta1"
type: "theme"

libraries:
juicebox:
download:
type: "file"
url: "https://www.dropbox.com/s/hrthl8t1r9cei5k/juicebox.zip?dl=1"

(once this project goes live, we will pin the version numbers)

drupal-core.make

core: "8.x"

api: 2

projects:
  drupal:
    version: 8.0.0
    patch:
      - https://www.drupal.org/files/issues/2611758-2.patch

prod.make

core: "8.x"

api: 2

includes:

- "apa-cms.make"

projects:
apa_profile:
type: "profile"
subdir: "."
download:
type: "copy"
url: "file://./drupal/profiles/apa_profile"

At the root of our project we also have a Gemfile, specifically to install the compass compiler along with various sass libraries. We install these tools on the host machine, and "watch" those directories from the host. vagrant rsync-auto watches any changed files and rsyncs them to the drupal8-container.

bundler

From the project root, installing these dependencies and running a compass watch is simple:

$ bundle
$ bundle exec compass watch path/to/theme

bower

We pull in any 3rd party front-end libraries such as Foundation, Font Awesome, etc... using Bower. From within the theme directory:

\$ bower install

There are a few things we do not commit to the application repo, as a result of the above commands.

  • The CSS directory
  • Bower Components directory

Deploy process

As I stated earlier, we utilize Jenkins CI to build an artifact that we can deploy. Within the jenkins job that handles deploys, each of the above steps is executed, to create a document root that can be deployed. Projects that we build to work on Acquia or Pantheon actually have a build step to also push the updated artifact to their respected repositories at the host, to take advantage of the automation that Pantheon and Acquia provide.

Conclusion

Although this wasn't an exhaustive walk thru of how we structure and build sites using Drupal, it should give you a general idea of how we do it. If you have specific questions as to why we go through this entire build process just to setup Drupal, please leave a comment. I would love to continue the conversation.

Look out for a video on this topic in the next coming weeks. I covered a lot in this post, without going into much detail. The intent of this post was to give a 10,000 foot view of the process. The upcoming video on this process will get much closer to the Tarmac!

As an aside, one caveat that we did run into with setting up default configuration in our Installation Profile was with Configuration Management UUID's. You can only sync configuration between sites that are clones. We have overcome this limitation with a workaround in our installation profile. I'll leave that topic for my next blog post in a few weeks.

Nov 14 2015
Nov 14

Shoov.io is a nifty website testing tool created by Gizra. We at ActiveLAMP were first introduced to Shoov.io at DrupalCon LA, in fact, Shoov.io is built on, you guessed it, Drupal 7 and it is an open source visual regression toolkit.

Shoov.io uses webdrivercss, graphicsmagick, and a few other libraries to compare images. Once the images are compared you can visually see the changes in the Shoov.io online app. When installing Shoov you can choose to install it directly into your project directory/repository or you can use a separate directory/repository to house all of your tests and screenshots. Initially when testing Shoov we had it contained in a separate directory but with our most recent project, we opted to install Shoov directly into our project with the hopes to have it run on a commit or pull request basis using Travis CI and SauceLabs.

[embedded content]

Installation

To get Shoov installed into your project, I will, for this install, assume that you want to install it into your project, navigate into your project using the terminal.

Install the Yeoman Shoov generator globally (may have to sudo)

npm install -g mocha yo generator-shoov

Make sure you have Composer installed globally

curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer

Make sure you have Brew installed (MacOSX)

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Install GraphicsMagick (MacOSX)

brew install graphicsmagick

Next you will need to install dependencies if you don't have them already.

npm install -g yo npm install -g file-type

Now we can build the test suite using the Yeoman generator*

yo shoov --base-url=http://activelamp.com

*running this command may give you more dependencies you need to install first.

This generator will scaffold all of the directories that you need to start writing tests for your project. It will also give you some that you may not need at the moment, such as behat. You will find the example test within the directory test in the visual-monitor directory named test.js. We like to split our tests into multiple files so we might rename our test to homepage.js. Here is what the homepage.js file looks like when you first open it.

'use strict'

var shoovWebdrivercss = require('shoov-webdrivercss')






var capsConfig = {
  chrome: {
    browser: 'Chrome',
    browser_version: '42.0',
    os: 'OS X',
    os_version: 'Yosemite',
    resolution: '1024x768',
  },
  ie11: {
    browser: 'IE',
    browser_version: '11.0',
    os: 'Windows',
    os_version: '7',
    resolution: '1024x768',
  },
  iphone5: {
    browser: 'Chrome',
    browser_version: '42.0',
    os: 'OS X',
    os_version: 'Yosemite',
    chromeOptions: {
      mobileEmulation: {
        deviceName: 'Apple iPhone 5',
      },
    },
  },
}

var selectedCaps = process.env.SELECTED_CAPS || undefined
var caps = selectedCaps ? capsConfig[selectedCaps] : undefined

var providerPrefix = process.env.PROVIDER_PREFIX
  ? process.env.PROVIDER_PREFIX + '-'
  : ''
var testName = selectedCaps
  ? providerPrefix + selectedCaps
  : providerPrefix + 'default'

var baseUrl = process.env.BASE_URL
  ? process.env.BASE_URL
  : 'http://activelamp.com'

var resultsCallback = process.env.DEBUG
  ? console.log
  : shoovWebdrivercss.processResults

describe('Visual monitor testing', function() {
  this.timeout(99999999)
  var client = {}

  before(function(done) {
    client = shoovWebdrivercss.before(done, caps)
  })

  after(function(done) {
    shoovWebdrivercss.after(done)
  })

  it('should show the home page', function(done) {
    client
      .url(baseUrl)
      .webdrivercss(
        testName + '.homepage',
        {
          name: '1',
          exclude: [],
          remove: [],
          hide: [],
          screenWidth: selectedCaps == 'chrome' ? [640, 960, 1200] : undefined,
        },
        resultsCallback
      )
      .call(done)
  })
})

Modifications

We prefer not to repeat configuration in our projects. We move the configuration setup to a file outside of the test folder and require it. We make this file by copying and removing the config from the above file and adding module.exports for each of the variables. Our config file looks like this

var shoovWebdrivercss = require('shoov-webdrivercss')






var capsConfig = {
  chrome: {
    browser: 'Chrome',
    browser_version: '42.0',
    os: 'OS X',
    os_version: 'Yosemite',
    resolution: '1024x768',
  },
  ie11: {
    browser: 'IE',
    browser_version: '11.0',
    os: 'Windows',
    os_version: '7',
    resolution: '1024x768',
  },
  iphone5: {
    browser: 'Chrome',
    browser_version: '42.0',
    os: 'OS X',
    os_version: 'Yosemite',
    chromeOptions: {
      mobileEmulation: {
        deviceName: 'Apple iPhone 5',
      },
    },
  },
}

var selectedCaps = process.env.SELECTED_CAPS || undefined
var caps = selectedCaps ? capsConfig[selectedCaps] : undefined

var providerPrefix = process.env.PROVIDER_PREFIX
  ? process.env.PROVIDER_PREFIX + '-'
  : ''
var testName = selectedCaps
  ? providerPrefix + selectedCaps
  : providerPrefix + 'default'

var baseUrl = process.env.BASE_URL
  ? process.env.BASE_URL
  : 'http://activelamp.com'

var resultsCallback = process.env.DEBUG
  ? console.log
  : shoovWebdrivercss.processResults

module.exports = {
  caps: caps,
  selectedCaps: selectedCaps,
  testName: testName,
  baseUrl: baseUrl,
  resultsCallback: resultsCallback,
}

Once we have this setup, we need to require it into our test and rewrite the variables from our test to make it work with the new configuration file. That file now looks like this.

'use strict'

var shoovWebdrivercss = require('shoov-webdrivercss')
var config = require('../configuration.js')

describe('Visual monitor testing', function() {
  this.timeout(99999999)
  var client = {}

  before(function(done) {
    client = shoovWebdrivercss.before(done, config.caps)
  })

  after(function(done) {
    shoovWebdrivercss.after(done)
  })

  it('should show the home page', function(done) {
    client
      .url(config.baseUrl)
      .webdrivercss(
        config.testName + '.homepage',
        {
          name: '1',
          exclude: [],
          remove: [],
          hide: [],
          screenWidth:
            config.selectedCaps == 'chrome' ? [640, 960, 1200] : undefined,
        },
        config.resultsCallback
      )
      .call(done)
  })
})

Running the test

Now we can run our test. For initial testing, if you don't have a BrowserStack account or SauceLabs, you can test using phantom js

Note: You must have a repository or the test will fail.

In another terminal window run:

phantomjs --webdriver=4444

Return to the original terminal window and run:

SELECTED_CAPS=chrome mocha

This will run the tests specified for "chrome" in the configuration file and the screenWidths from within each test as specified by the default test.

Once the test runs you should see that it has passed. Of course, our test passed because we didn't have anything to compare it to. This test will create your initial baseline images. You will want to review these images in the webdrivercss directory and decide if you need to fix your site, your tests, or both. You may have to remove, exclude or hide elements from your tests. Removing an element will completely rip it from the dom for the test and will shift your site around. Excluding will create a black box over the content that you want to not show up, this is great for areas that you want to keep a consistent layout and the item is a fixed size. Hiding an element will hide the element from view, works similar to remove but works better with child elements outside of the parent. Once you review the baseline images you may want to take the time to commit and push the new images to GitHub (this commit will be the one that appears in the interface later)

Comparing the Regressions

Once you modify your test or site you can test it against the baseline that exists. Now that you probably have a regression you can go to the Shoov interface. From within the interface, you will select Visual Regression. The commit from your project will appear in a list and you will click the commit to be able to view the regressions and take action on any other issues that exist or you can save your new baseline. Only images with a regression will show up in the interface and only tests with regressions will show up on the list.

What's Next

You can view the standalone GitHub repository here.

This is just the tip of the iceberg for us with Visual Regression testing. We hope to share more about our testing process and how we are using Shoov for our projects. Don't forget to share or comment if you like this post.

Oct 17 2015
Oct 17

A little over a year ago the ActiveLAMP website had undergone a major change -- we made the huge decision of moving away from using Drupal to manage its content in favor of building it as a static HTML site using Jekyll, hosted on Amazon S3. Not only did this extremely simplify our development stack, it also trimmed down our server requirements to the very bare minimum. Now, we are just hosting everything on a file storage server like it's 1993.

A few months ago we identified the need to restructure our URL schemes as part of an ongoing SEO campaign. As easy as that sounds, this, however, necessitates the implementation of 301 redirects from the older URL scheme to their newer, more SEO-friendly versions.

I'm gonna detail how I managed to (1) implement these redirects quite easily using an nginx service acting as a proxy, and (2) achieve parity between our local and production environments while keeping everything light-weight with the help of Docker.

Nginx vs Amazon S3 Redirects

S3 is a file storage service offered by AWS that not only allows you to store files but also allows you to host static websites in conjunction with Route 53. Although S3 gives you the ability to specify redirects, you'll need to use S3-specific configuration and routines. This alone wouldn't be ideal because not only would it tie us to S3 by the hips, but it is not a methodology that we could apply to any other environment (i.e. testing and dev environments on our local network and machines). For these reasons, I opted to use nginx as a very thin reverse proxy to accomplish the job.

Configuring Nginx

Rather than compiling the list of redirects manually, I wrote a tiny Jekyll plugin that can do it faster and more reliably. The plugin allows me to specify certain things within the main Jekyll configuration file and it will generate the proxy.conf file for me:



nginx:
    proxy_host: ></span>;
    proxy_port: 80
    from_format: "/:year/:month/:day/:title/"
    
    redirects:
        - { from: "^/splash(.*)", to: "/$1" type: redirect }

With this in place, I am able to generate the proxy.conf by simply issuing this command:

> jekyll nginx_config > proxy.conf

This command will produce a proxy.conf file which will look like this:



rewrite ^/2008/09/21/drupalcampla\-revision\-control\-presentation/?(\?.*)?$ /blog/drupal/drupalcampla-revision-control-presentation$1 permanent;




rewrite ^/blog/development/aysnchronous\-php\-with\-message\-queues/?(\?.*)?$ /blog/development/asynchronous-php-with-message-queues/$1 permanent;


location / {
	proxy_set_header Host <S3 bucket URL>;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_pass http://<S3 bucket URL>:80;
}

You probably noticed that this is not a complete nginx configuration. However all that is need to be done is to define a server directive which will import this file:



server {
    listen 80;
    include /etc/nginx/proxy.conf;
}

Here we have an nginx proxy listening on port 80 that knows how to redirect old URLs to the new ones and pass on any request to the S3 bucket on AWS.

From this point, I was able to change the DNS records on our domain to point to the nginx proxy service instead of pointing directly at S3.

Check out the documentation for more ways of specifying redirects and more advanced usage.

Docker

Spinning up the proxy locally is a breeze with the help of Docker. Doing this in Vagrant and a provisioner would require a lot of boilerplate and code. With Docker, everything (with the exception of the config file that is automatically generated), is under 10 lines!

The Dockerfile

Here I am using the official nginx image straight from DockerHub but added some minor modifications:



FROM nginx


RUN rm /etc/nginx/conf.d/default.conf


ADD server.conf /etc/nginx/conf.d/
ADD proxy.conf /etc/nginx/

The build/nginx directory will contain everything the nginx proxy will need: the server.conf that you saw from the previous section, and the proxy.conf file which was generated by the jekyll nginx_config command.

Automating it with Grunt

Since we are using generator-jekyllrb, a Yeoman generator for Jekyll sites which uses Grunt to run a gamut of various tasks, I just had to write a grunt proxy task which does all the needed work when invoked:



...

grunt.initConfig({
    ...
    local_ip: process.env.LOCAL_IP,
    shell: {
        nginx_config: {
            command: 'jekyll nginx_config --proxy_host=192.168.0.3
            --proxy_port=8080 --config=_config.yml,_config.build.yml --source=app > build/nginx/proxy.conf'
        }
        docker_build: {
            command: 'docker build -t jekyll-proxy build/nginx'
        },
        docker_run: {
            command: 'docker run -d -p 80:80 jekyll-proxy'
        }
    },
    ...
});

...

grunt.registerTask('proxy', [
      'shell:nginx_config',
      'shell:docker_build',
      'shell:docker_run'
]);

This requires grunt-shell

With this in place, running grunt proxy will prepare the configuration, build the image, and run the proxy on http://192.168.99.100 where 192.168.99.100 is the address to the Docker host VM on my machine.

Note that this is a very simplified version of the actual Grunt task config that we actually use which just serves to illustrate the meager list of commands that is required to get the proxy configured and running.

I have set up a GitHub repository that replicates this set-up plus the actual Grunt task configuration we use that adds more logic around things like an auto-rebuilding the Docker image, cleaning up of stale Docker processes, configuration for different build parameters for use in production, etc. You can find it here: bezhermoso/jekyll-nginx-proxy-with-docker-demo.

Oct 10 2015
Oct 10

Running headless Drupal with a separate javascript framework on the front-end can provide amazing user experiences and easy theming. Although, working with content editors with this separation can prove to be a tricky situation.

Problem

User story As part of the content team, I need to be able to see a preview of the article on a separate front-end (Ember) application without ever saving the node.

As I started reading this user story I was tasked with, the wheels started turning as to how I would complete this. I had to get information not saved anywhere to an Ember front-end application with no POST abilities. I love challenges that take a lot of thought before jumping in. I talked with some of the other developers here and came up with a pretty nifty solution to the problem, which come to find out was just a mis-communication on the story... and when I was done the client was still excited at the possibilities it could open up; but keep in mind this was still in it's early stages and there are still a lot of holes to fill.

Solution

The first thing I did was add a Preview link and attach a javascript file to the node (article type) edit page via a simple hook_form_FORM_ID_alter().



 function mymodule_form_article_node_form_alter(&$form, &$form_state, $form_id) {
	$form['#attached']['js'][] = drupal_get_path('module', 'mymodule') . '/js/preview.js'
	$form['actions']['preview_new'] = [
		'#weight' => '40',
		'#markup' => 'Preview on front-end'
	];
}

Pretty simple. Since it was just the beginning stages I just threw it in an anchor tag to get the link there. So it wasn't pretty. Now to start gathering the node's data since it won't be saved anywhere.

Next step: Gather node info in js

So obviously the next step was to gather up the node's information from the node edit screen to prepare for shipping to the front-end application. Most time spent here was just trying to mimic the api that the Ember front-end was already expecting.


(function ($, Drupal) {
  Drupal.behaviors.openNewSite = {
    attach : function(context, settings) {
      viewInNewSite.init();
    }
  }
})(jQuery, Drupal);

var viewInNewSite = (function($) {

  
  var getMedia = function() {...};

  
  var getMessage = function() {...};

 
  var attachPreviewClick = function(link) {
    var $link = $(link);
    $link.on('click', function() {
      var newWindow = window.open('http://the-front-end.com/articles/preview/preview-article');
      var message = getMessage();
      
      setTimeout(function() {
        newWindow.postMessage(JSON.stringify(message), 'http://the-front-end.com');
      }, 1000);
    });
  };

  
  var init = function() {
    attachPreviewClick('#preview-new');
  };

  return {
    init: init
  }
})(jQuery);

Using window.open() to open what will be the route in the Ember application returns the newWindow for us to post a message to. (I used a setTimeout() here because the opening application took a while to get started and the message would get missed... this held me up for a while since I knew the message should be getting sent.) Then using postMessage() on newWindow to ship off a message (our json object) to the opening window, regardless of if it is another domain. Insert security concerns here... but now we're ready to setup the front end Ember application route.

To Ember!

The next step was to set up the Ember front-end application to listen for the message from the original window. Set up the basic route:


this.resource('articles', function() {
  this.route('preview', { path: '/preview/article-preview' })
  this.route('article', { path: '/:article_id' })
})

The application that I was previewing articles into already had a way to view articles by id as you see in the above code. So I didn't want to have to duplicate anything... I wanted to use the same template and model for articles that were already developed. Since that was taken care of for me, it was just time to create a model for this page and make sure that I use the correct template. So start by creating the ArticlesPreviewRoute:

App.ArticlesPreviewRoute = (function() {
 return Ember.Route.extend({

renderTemplate: function() {
this.render('articles.article');
},
controllerName: 'articles.article',

setupController: function(controller, model) {
controller.set('model', model);
},
model: function (params) {
return getArticle();
}
});

/\*\*

- Adds event listener to the window for incoming messages.
-
- @return {Promise}
  \*/
  var getArticle = function() {
  return new Promise(function(resolve, reject) {
  window.addEventListener('message', function(event) {
  if (event.origin.indexOf('the-back-end.com') !== -1) {
  var data = JSON.parse(event.data);
  resolve(data.articles[0]);
  }
  }, false);
  });
  };
  })();

The getArticle() function above returns a new Promise and adds an event listener that verifies that the message is coming from the correct origin. Clicking the link from Drupal would now take content to a new tab and load up the article. There would be some concerns that need to be resolved such as security measures and if a user visits the path directly.

To cover the latter concern, a second promise would either resolve the promise or reject it if the set amount of time has passed without a message coming from the origin window.

App.ArticlesPreviewRoute = (function() {
  return Ember.Route.extend({
    ...
    model: function (params) {
	  var self = this;
      var article = previewWait(10, getArticle()).catch(function() {
        self.transitionTo('not-found');
      });
      return article;
    }
  });

  var getArticle = function() {
	return new Promise(function(resolve, reject) {...}
  };

  
  var previewWait = function(seconds, promise) {
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
        reject({'Bad': 'No data Found'});
      }, seconds * 1000);
      promise.then(function(data) {
        resolve(data);
      }).catch(reject);
    });
  };
})();

There you have it! A way to preview an article from a Drupal backend to an Ember front-end application without ever saving the article. A similar approach could be used for any of your favorite Javascript frameworks. Plus, this can be advanced even further into an "almost live" updating front-end that constantly checks the state of the fields on the Drupal backend. There have been thoughts of turning this into a Drupal module with some extra bells and whistles for configuring the way the json object is structured to fit any front-end framework... Is there a need for this? Let us know in the comments below or tweet us! Now, go watch the video!

Sep 23 2015
Sep 23

This post is part 3 in the series "Hashing out a docker workflow". For background, checkout my previous posts.

Now that I've laid the ground work for the approach that I want to take with local environment development with Docker, it's time to explore how to make the local environment "workable". In this post we will we will build on top of what we did in my last post, Docker and Vagrant, and create a working local copy that automatically updates the code inside the container running Drupal.

Requirements of a local dev environment

Before we get started, it is always a good idea to define what we expect to get out of our local development environment and define some requirements. You can define these requirements however you like, but since ActiveLAMP is an agile shop, I'll define our requirements as users stories.

User Stories

As a developer, I want my local development environment setup to be easy and automatic, so that I don't have to spend the entire day following a list of instructions. The fewer the commands, the better.

As a developer, my local development environment should run the same exact OS configuration as stage and prod environments, so that we don't run into "works on my machine" scenario's.

As a developer, I want the ability to log into the local dev server / container, so that I can debug things if necessary.

As a developer, I want to work on files local to my host filesystem, so that the IDE I am working in is as fast as possible.

As a developer, I want the files that I change on my localhost to automatically sync to the guest filesystem that is running my development environment, so that I do not have to manually push or pull files to the local server.

Now that we know what done looks like, let's start fulfilling these user stories.

Things we get for free with Vagrant

We have all worked on projects that have a README file with a long list of steps just to setup a working local copy. To fulfill the first user story, we need to encapsulate all steps, as much as possible, into one command:

\$ vagrant up

We got a good start on our one command setup in my last blog post. If you haven't read that post yet, go check it out now. We are going to be building on that in this post. My last post essentially resolves the first three stories in our user story list. This is the essence of using Vagrant, to aid in setting up virtual environments with very little effort, and dispose them when no longer needed with vagrant up and vagrant destroy, respectively.

Since we will be defining Docker images and/or using existing docker containers from DockerHub, user story #2 is fulfilled as well.

For user story #3, it's not as straight forward to log into your docker host. Typically with vagrant you would type vagrant ssh to get into the virtual machine, but since our host machine's Vagrantfile is in a subdirectory called /host, you have to change directory into that directory first.

$ cd host
$ vagrant ssh

Another way you can do this is by using the vagrant global-status command. You can execute that command from anywhere and it will provide a list of all known virtual machines with a short hash in the first column. To ssh into any of these machines just type:

\$ vagrant ssh <short-hash>

Replace with the actual hash of the machine.

Connecting into a container

Most containers run a single process and may not have an SSH daemon running. You can use the docker attach command to connect to any running container, but beware if you didn't start the container with a STDIN and STDOUT you won't get very far.

Another option you have for connecting is using docker exec to start an interactive process inside the container. For example, to connect to the drupal-container that we created in my last post, you can start an interactive shell using the following command:

$ sudo docker exec -t -i drupal-container /bin/bash

This will return an interactive shell on the drupal-container that you will be able to poke around on. Once you disconnect from that shell, the process will end inside the container.

Getting files from host to app container

Our next two user stories have to do with working on files native to the localhost. When developing our application, we don't want to bake the source code into a docker image. Code is always changing and we don't want to rebuild the image every time we make a change during the development process. For production, we do want to bake the source code into the image, to achieve the immutable server pattern. However in development, we need a way to share files between our host development machine and the container running the code.

We've probably tried every approach available to us when it comes to working on shared files with vagrant. Virtualbox shared files is just way too slow. NFS shared files was a little bit faster, but still really slow. We've used sshfs to connect the remote filesystem directly to the localhost, which created a huge performance increase in terms of how the app responded, but was a pain in the neck in terms of how we used VCS as well as it caused performance issues with the IDE. PHPStorm had to index files over a network connection, albiet a local network connection, but still noticebly slower when working on large codebases like Drupal.

The solution that we use to date is rsync, specifically vagrant-gatling-rsync. You can checkout the vagrant gatling rsync plugin on github, or just install it by typing:

\$ vagrant plugin install vagrant-gatling-rsync

Syncing files from host to container

To achieve getting files from our localhost to the container we must first get our working files to the docker host. Using the host Vagrantfile that we built in my last blog post, this can be achieved by adding one line:

config.vm.synced_folder '../drupal/profiles/myprofile', '/srv/myprofile', type: 'rsync'

Your Vagrantfile within the /host directory should now look like this:





Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.hostname = "docker-host"
config.vm.provision "docker"
config.vm.network :forwarded_port, guest: 80, host: 4567
config.vm.synced_folder '../drupal/profiles/myprofile', '/srv/myprofile', type: 'rsync'
end

We are syncing a drupal profile from a within the drupal directory off of the project root to a the /srv/myprofile directory within the docker host.

Now it's time to add an argument to run when docker run is executed by Vagrant. To do this we can specify the create_args parameter in the container Vagrant file. Add the following line into the container Vagrantfile:

docker.create_args = ['--volume="/srv/myprofile:/var/www/html/profiles/myprofile"']

This file should now look like:





Vagrant.configure(2) do |config|
config.vm.provider "docker" do |docker|
docker.vagrant_vagrantfile = "host/Vagrantfile"
docker.image = "drupal"
docker.create_args = ['--volume="/srv/myprofile:/var/www/html/profiles/myprofile"']
docker.ports = ['80:80']
docker.name = 'drupal-container'
end
end

This parameter that we are passing maps the directory we are rsyncing to on the docker host to the profiles directory within the Drupal installation that was included in the Drupal docker image from DockerHub.

Create the installation profile

This blog post doesn't intend to go into how to create a Drupal install profile, but if you aren't using profiles for building Drupal sites, you should definitely have a look. If you have questions regarding why using Drupal profiles are a good idea, leave a comment.

Lets create our simple profile. Drupal requires two files to create a profile. From the project root, type the following:

$ mkdir -p drupal/profiles/myprofile
$ touch drupal/profiles/myprofile/{myprofile.info,myprofile.profile}

Now edit each file that you just created with the minimum information that you need.

myprofile.info

name = Custom Profile
description = My custom profile
core = 7.x

myprofile.profile


function myprofile_install_tasks() {
  
}

Start everything up

We now have everything we need in place to just type vagrant up and also have a working copy. Go to the project root and run:

\$ vagrant up

This will build your docker host as well as create your drupal container. As I mentioned in a previous post, starting up the container sometimes requires me to run vagrant up a second time. I'm still not sure what's going on there.

After everything is up and running, you will want to run the rsync-auto command for the docker host, so that any changes you make locally traverses down to the docker host and then to the container. The easiest way to do this is:

$ cd host
$ vagrant gatling-rsync-auto

Now visit the URL to your running container at http://localhost:4567 and you should see the new profile that you've added.

Custom Install Profile

Conclusion

We covered a lot of ground in this blog post. We were able to accomplish all of the stated requirements above with just a little tweaking of a couple Vagrantfiles. We now have files that are shared from the host machine all the way down to the container that our app is run on, utilizing features built into Vagrant and Docker. Any files we change in our installation profile on our host immediately syncs to the drupal-container on the docker host.

At ActiveLAMP, we use a much more robust approach to build out installation profiles, utilizing Drush Make, which is out of scope for this post. This blog post simply lays out the general idea of how to accomplish getting a working copy of your code downstream using Vagrant and Docker.

In my next post, I'll continue to build on what I did here today, and introduce automation to automatically bake a Docker image with our source code baked in, utilizing Jenkins. This will allow us to release any number of containers easily to scale our app out as needed.

Sep 22 2015
D
Sep 22

RNG has tagged numerous alpha releases and maintained stability with Drupal core. Because of this stability I will be accellerating to 1.0 by skipping beta and going straight to RC. However RC will not occur until the following requirements are satisfied:

Everything should line up for a RNG RC in October or November 2015.

Updated October 20, 2015: All requirements were satisfied, the first RNG release candidate was released on October 14th, 2015.

Cover photo (unmodified): Running gear of steam locomotive by Petar Milošević. License CC BY-SA 3.0

Aug 14 2015
Aug 14

I recently had time to install and take a look at Drupal 8. I am going to share my first take on Drupal 8 and some of the hang-ups that I came across. I read a few other blog posts that mentioned not to rely too heavily on one source for D8 documentation with the rapid changing pace of D8 the information has become outdated rather quickly.

Getting Started

My first instinct was to run over to drupal.org and grab a copy of the code base and set it up on MAMP. Then I saw an advertisement for running Drupal 8 on Acquia Cloud Free and decided that would probably be a great starting point. Running through the setup for Acquia took only about eight minutes. This was great, having used Acquia and Pantheon before this was an easy way to get started.

Next, I decided to pull down the code and get it running locally so that I could start testing out adding my own theme. Well... What took 8 minutes for Acquia took relatively longer for me.

Troubleshooting and Upgrading

The first roadblock that I ran into was that my MAMP was not running the required version of PHP ( 5.5.9 or higher) and I decided to upgrade to MAMP 3 to make life a little bit nicer. After setting up MAMP from scratch and making sure the other projects that I had installed with MAMP still work correctly I was able to continue with the site install.

The second roadblock that I came across was not having Drush 7+ installed. It doesn't come out and say in the command line that you need to upgrade Drush (it does in the docs on drupal.org and if you search the error it is one of the first results). It just spits out this error:

Fatal error: Class 'Drupal\Core\Session\AccountInterface' not found in .../docroot/core/includes/bootstrap.inc on line 64
Drush command terminated abnormally due to an unrecoverable error.

The next roadblock was that I was trying to clear cache with Drush and didn't bother to read the documentation on drupal.org that outlined that drush cc all no longer exists and is replaced by drush cr. Drush now uses the cache-rebuild command. However, this is not exactly clear given that if you run drush cc all you get the same exact error as the one above.

Finally everything was setup and working properly. I decided to look around for a good guide to jumpstart getting the theme setup and landed here at this Lullabot article. For the most part, the article was straight forward. Some parts didn't work and I skipped and others didn't work and I tried to figure out why. Here is the list of things that I couldn't figure out:

  • Drush wouldn't change the default theme (complained about bootstrap level even though I was in my docroot)
  • Stylesheets-remove didn't work inside of my theme.info.yml file
  • Specifying my CSS in my theme.libraries.yml file seemed to be problematic but got it working after some time. (probably user error)

Conclusion

Drupal 8 looks clean and feels sleek and slimmed down. I'm really excited for the direction that Drupal is headed. Overall the interface within Drupal hasn't changed too drastically (maybe some naming conventions ie. extend over modules). It looks like one of our current sites running Panopoly which has a great look and feel over out-of-the-box D7. I really like the simplicity of separating out yml files for specific needs and setting up the theme.

I look forward to writing more blog posts about Drupal 8 and maybe some tutorials and insights. Let us know your thoughts and ideas in the comments section below.

Jul 19 2015
Jul 19

This post is part 2 in a series of Docker posts hashing out a new docker workflow for our team. To gain background of what I want to accomplish with docker, checkout my previous post hashing out a docker workflow.

In this post, we will venture into setting up docker locally, in the same repeatable way from developer to developer, by using Vagrant. By the end of this post, we'll have Drupal running in a container, using Docker. This post is focused on hashing out a Docker workflow with Vagrant, less about Drupal itself. I want to give a shout out to the folks that maintain the Drupal Docker image in the Docker Registry. It's definitely worth checking out, and using that as a base to build FROM for your custom images.

Running Docker Locally

There are several ways to go about setting up Docker locally. I don't intend to walk you through how to install Docker, you can find step-by-step installation instructions based on your OS in the Docker documentation. However, I am leaning toward taking an unconventional approach to installing Docker locally, not mentioned in the Docker documentation. Let me tell you why.

Running a Docker host

For background, we are specifically an all Mac OS X shop at ActiveLAMP, so I'll be speaking from this context.

Unfortunately you can't run Docker natively on OS X, Docker needs to run in a Virtual Machine with an operating system such as Linux. If you read the Docker OS X installation docs, you see there are two options for running Docker on Mac OS X, Boot2Docker or Kitematic.

Running either of these two packages looks appealing to get Docker up locally very quickly (and you should use one of these packages if you're just trying out Docker), but thinking big picture and how we plan to use Docker in production, it seems that we should take a different approach locally. Let me tell you why I think you shouldn't use Boot2Docker or Kitematic locally, but first a rabbit trail.

Thinking ahead (the rabbit trail)

My opinion may change after gaining more real world experience with Docker in production, but the mindset that I'm coming from is that in production our Docker hosts will be managed via Chef.

Our team has extensive experience using Chef to manage infrastructure at scale. It doesn't seem quite right to completely abandon Chef yet, since Docker still needs a machine to run the Docker host. Chef is great for machine level provisioning.

My thought is that we would use Chef to manage the various Docker hosts that we deploy containers to and use the Dockerfile with Docker Compose to manage the actual app container configuration. Chef would be used in a much more limited capacity, only managing configuration on a system level not an application level. One thing to mention is that we have yet to dive into the Docker specific hosts such as AWS ECS, dotCloud, or Tutum. If we end up adopting a service like one of these, we may end up dropping Chef all together, but we're not ready to let go of those reigns yet.

One step at a time for us. The initial goal is to get application infrastructure into immutable containers managed by Docker. Not ready to make a decision on what is managing Docker or where we are hosting Docker, that comes next.

Manage your own Docker Host

The main reason I was turned off from using Boot2Docker or Kitematic is that it creates a Virtual Machine in Virtualbox or VMWare from a default box / image that you can't easily manage with configuration management. I want control of the host machine that Docker is run on, locally and in production. This is where Chef comes into play in conjunction with Vagrant.

Local Docker Host in Vagrant

As I mentioned in my last post, we are no stranger to Vagrant. Vagrant is great for managing virtual machines. If Boot2Docker or Kitematic are going to spin up a virtual machine behind the scenes in order to use Docker, then why not spin up a virtual machine with Vagrant? This way I can manage the configuration with a provisioner, such as Chef. This is the reason I've decided to go down the Vagrant with Docker route, instead of Boot2Docker or Kitematic.

The latest version of Vagrant ships with a Docker provider built-in, so that you can manage Docker containers via the Vagrantfile. The Vagrant Docker integration was a turn off to me initially because it didn't seem it was very Docker-esque. It seemed Vagrant was just abstracting established Docker workflows (specifically Docker Compose), but in a Vagrant syntax. However within the container Vagrantfile, I saw you can also build images from a Dockerfile, and launch those images into a container. It didn't feel so distant from Docker any more.

It seems that there might be a little overlap in areas between what Vagrant and Docker does, but at the end of the day it's a matter of figuring out the right combination of using the tools together. The boundary being that Vagrant should be used for "orchestration" and Docker for application infrastructure.

When all is setup we will have two Vagrantfiles to manage, one to define containers and one to define the host machine.

Setting up the Docker Host with Vagrant

The first thing to do is to define the Vagrantfile for your host machine. We will be referencing this Vagrantfile from the container Vagrantfile. The easiest way to do this is to just type the following in an empty directory (your project root):

\$ vagrant init ubuntu/trusty64

You can configure that Vagrantfile however you like. Typically you would also use a tool like Chef solo, Puppet, or Ansible to provision the machine as well. For now, just to get Docker installed on the box we'll add to the Vagrantfile a provision statement. We will also give the Docker host a hostname and a port mapping too, since we know we'll be creating a Drupal container that should EXPOSE port 80. Open up your Vagrantfile and add the following:

config.vm.hostname = "docker-host"
config.vm.provision "docker"
config.vm.network :forwarded_port, guest: 80, host: 4567

This ensures that Docker is installed on the host when you run vagrant up, as well as maps port 4567 on your local machine to port 80 on the Docker host (guest machine). Your Vagrantfile should look something like this (with all the comments removed):





Vagrant.configure(2) do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.hostname = "docker-host"
config.vm.provision "docker"
config.vm.network :forwarded_port, guest: 80, host: 4567
end

Note: This post is not intended to walk through the fundamentals of Vagrant, for further resources on how to configure the Vagrantfile check out the docs.

As I mentioned earlier, we are going to end up with two Vagrantfiles in our setup. I also mentioned the container Vagrantfile will reference the host Vagrantfile. This means the container Vagrantfile is the configuration file we want used when vagrant up is run. We need to move the host Vagrantfile to another directory within the project, out of the project root directory. Create a host directory and move the file there:

$ mkdir host
$ mv Vagrantfile !\$

Bonus Tip: The !$ destination that I used when moving the file is a shell shortcut to use the last argument from the previous command.

Define the containers

Now that we have the host Vagrantfile defined, lets create the container Vagrantfile. Create a Vagrantfile in the project root directory with the following contents:





Vagrant.configure(2) do |config|
config.vm.provider "docker" do |docker|
docker.vagrant_vagrantfile = "host/Vagrantfile"
docker.image = "drupal"
docker.ports = ['80:80']
docker.name = 'drupal-container'
end
end

To summarize the configuration above, we are using the Vagrant Docker provider, we have specified the path to the Docker host Vagrant configuration that we setup earlier, and we defined a container using the Drupal image from the Docker Registry along with exposing some ports on the Docker host.

Start containers on vagrant up

Now it's time to start up the container. It should be as easy as going to your project root directory and typing vagrant up. It's almost that easy. For some reason after running vagrant up I get the following error:

A Docker command executed by Vagrant didn't complete successfully!
The command run along with the output from the command is shown
below.

Command: "docker" "ps" "-a" "-q" "--no-trunc"

Stderr: Get http:///var/run/docker.sock/v1.19/containers/json?all=1: dial unix /var/run/docker.sock: permission denied. Are you trying to connect to a TLS-enabled daemon without TLS?

Stdout:

I've gotten around this is by just running vagrant up again. If anyone has ideas what is causing that error, please feel free to leave a comment.

Drupal in a Docker Container

You should now be able to navigate to http://localhost:4567 to see the installation screen of Drupal. Go ahead and install Drupal using an sqlite database (we didn't setup a mysql container) to see that everything is working. Pretty cool stuff!

Development Environment

There are other things I want to accomplish with our local Vagrant environment to make it easy to develop on, such as setting up synced folders and using the vagrant rsync-auto tool. I also want to customize our Drupal builds with Drush Make, to make developing on Drupal much more efficient when adding modules, updating core, etc... I'll leave those details for another post, this post has become very long.

Conclusion

As you can see, you don't have to use Boot2Docker or Kitematic to run Docker locally. I would advise that if you just want to figure out how Docker works, then you should use one of these packages. Thinking longer term, your local Docker Host should be managed the same way your production Docker Host(s) are managed. Using Vagrant, instead of Boot2Docker or Kitematic, allows me to manage my local Docker Host similar to how I would manage production Docker Hosts using tools such as Chef, Puppet, or Ansible.

In my next post, I'll build on what we did here today, and get our Vagrant environment into a working local development environment.

Jul 03 2015
Jul 03

The Picture module is a backport of Drupal 8 Responsive Image module. It allows you to select different images to be loaded for different devices and resolutions using media queries and Drupal’s image styles. You can also use the Image Replace module to specify a different image to load at certain breakpoints.

The Picture module gives you the correct formatting for an HTML5 “” element and includes a polyfill library for backwards compatibility with unsupported browsers. Unfortunately, this includes IE, Edge, Safari, iOS Safari, and Opera Mini. For more information, Can I Use.

The picture module works great with views. It supports WYSIWYG and creditor. It can be implemented directly with code if necessary.

Installation

Picture has two important dependencies

  1. Breakpoints module
  2. Ctools module

You can install the Picture module using Drush.

You will also want the Media module and Image Replace module.

Setting Up

Once you have the modules installed and enabled you can start to configure your theme.

1. Set up your breakpoints

You can add breakpoint settings directly in Drupal under Configuration > Media > Breakpoints. Or if you prefer you can add them to the .info file of your theme. We added ours to our theme like this.

breakpoints[xxlarge] = (min-width: 120.063em)
breakpoints[xlarge]  = (min-width: 90.063em) and (max-width: 120em)
breakpoints[large]   = (min-width: 64.063em) and (max-width: 90em)
breakpoints[medium]  = (min-width: 40.063em) and (max-width: 64em)
breakpoints[small]   = (min-width: 0em) and (max-width: 40em)

2. Add Responsive Styles

From within the Drupal UI, you will want to go to Configuration > Media > Breakpoints

  1. Click “Add Responsive Style”.
  2. Select which breakpoints you want to use for this specific style.
  3. Then you will choose an existing image style from the drop-down list that this style will clone.
  4. Finally you give it a base name for your new image styles. I would recommend naming it something logical and ending it with “_”.

You can now edit your image styles, you can do your normal scale crop for different sizes or you can setup image replace.

3. Setting up Picture Mappings

Once you have your image styles you have to create a mapping for them to associate with if you go to Configuration > Media > Picture Mappings

  1. Click Add to add a new picture mapping
  2. Select your breakpoint group.
  3. Give your mapping an Administrative title (something you can pick out of a select list)
  4. Then you will select "use image style" and select each of the corresponding image styles that you setup from the select list that appears.
Once you have that created you can now select that mapping and use it in different areas of your site. For example, you can use it in the Display of a node using the Manage Display section and formatting it as picture.
You can use it in views by selecting picture format and selecting you picture mapping.

4. Setting up Image Replace

Click edit on your desired image style and as an effect you can add the Replace Image setting. Replace can be combined with other styles as well so if you need to scale and crop you can do that too

Once you have your style setup you need to specify a field to replace with the image with a specific dimensions. We added a secondary field to our structure. We named them with horizontal and vertical because it made sense for us because we only use the vertical field when we are at smaller widths and the content stretches move vertically. You can use whatever naming convention will work best for you.

On the main image that you added to your view or display you edit the field settings and there is a section called Image Replace. Find the image style you set Replace image on and select your field you want to replace your current field.

Finished Product

Once that is done you are all set. If you have any questions or comments be sure to leave your comment in the comments section below.

Here is an example of how we used image replace.

Jun 16 2015
D
Jun 16

After 5 months of development, RNG is ready for its first alpha release. A milestone where there are no major issues, and schema is not anticipated to change leading up until v1.0.

RNG is a Drupal 8 module implementing a core toolset for allowing users to register for events. An event can be a presentation at a conference, a meetup, a class. If you need a way to associate people with a Drupal entity, take a look at RNG.

Core features of RNG:

  • Any entity type — designate any bundle (e.g node type) as an event type.
  • Self and proxy registration — users can register themselves or other identities to the event.
  • Multiple registrants — registrations can associate multiple identities.
  • Messaging — messages, such as email, can be sent to registrants. Messages have token support, allowing you to recycle field values from the related event, identity, and registration. Messages can be timed, or sent when a registration is created.
  • Additional identities — the Identity module allows you to register non-users. It is modeled like an address book, where contacts are added, and then made available as an option on the registration form.

Theres still a few things that will make it in before 1.0 final. Including improved Views integration, and improved/full test coverage.

I work on RNG in my own time. If you want to help out, don't hesitate in posting in the issue queue or contacting me directly.

Need help getting setup? See Quick Start.

Download the module on drupal.org.

Cover photo: Seedling by sjg. License CC BY-NC 2.0

Jan 17 2015
Jan 17

Drupal Form Ajax Example

Why reload the whole page, when you can just update certain parts of the DOM? Ajax allows you to do just this, to dynamically update content. Just one of the many great uses of Ajax is Form Validation. In this example, we will see how to implement this.

We will be making a simple form which will contain a text field that will validate if the username entered exists, and a button that will replace the text field value with a random existing username.

Building The Form

First, we need to define our two form elements:

$form['user_name'] = array( '#type' => 'textfield', '#title' => 'Username', '#description' => 'Please enter in a username', ); $form['random_user'] = array( '#type' => 'button', '#value' => 'Random Username', ); $form['user_name'] = array(  '#type' => 'textfield',  '#title' => 'Username',  '#description' => 'Please enter in a username',$form['random_user'] = array(  '#type' => 'button',  '#value' => 'Random Username',

Next, to start using Ajax in Drupal, all you need to specify is the “callback”, or function to call, when the “event”, or trigger, is fired on that certain form element, in an array under the “#ajax” key:

$form['user_name'] = array( '#type' => 'textfield', '#title' => 'Username', '#description' => 'Please enter in a username', '#ajax' => array( // Function to call when event on form element triggered. 'callback' => 'Drupal\ajax_example\Form\AjaxExampleForm::usernameValidateCallback', // Javascript event to trigger Ajax. Currently for: 'onchange'. 'event' => 'change', ); $form['user_name'] = array(  '#type' => 'textfield',  '#title' => 'Username',  '#description' => 'Please enter in a username',  '#ajax' => array(    // Function to call when event on form element triggered.    'callback' => 'Drupal\ajax_example\Form\AjaxExampleForm::usernameValidateCallback',    // Javascript event to trigger Ajax. Currently for: 'onchange'.    'event' => 'change',

In the “callback”, include the full namespaced class and function you want to call. The event can be any Javascript event without the “on”. A list of Javascript events can be found here.

Once you have added these two keys, you can add extra options such as “effect”, and “progress”. More options can be found on the Ajax API. Here are the finished elements:

$form['user_name'] = array( '#type' => 'textfield', '#title' => 'Username', '#description' => 'Please enter in a username', '#ajax' => array( // Function to call when event on form element triggered. 'callback' => 'Drupal\ajax_example\Form\AjaxExampleForm::usernameValidateCallback', // Effect when replacing content. Options: 'none' (default), 'slide', 'fade'. 'effect' => 'fade', // Javascript event to trigger Ajax. Currently for: 'onchange'. 'event' => 'change', 'progress' => array( // Graphic shown to indicate ajax. Options: 'throbber' (default), 'bar'. 'type' => 'throbber', // Message to show along progress graphic. Default: 'Please wait...'. 'message' => NULL, ), ), ); $form['random_user'] = array( '#type' => 'button', '#value' => 'Random Username', '#ajax' => array( 'callback' => 'Drupal\ajax_example\Form\AjaxExampleForm::randomUsernameCallback', 'event' => 'click', 'progress' => array( 'type' => 'throbber', 'message' => 'Getting Random Username', ), ), ); $form['user_name'] = array(  '#type' => 'textfield',  '#title' => 'Username',  '#description' => 'Please enter in a username',  '#ajax' => array(    // Function to call when event on form element triggered.    'callback' => 'Drupal\ajax_example\Form\AjaxExampleForm::usernameValidateCallback',    // Effect when replacing content. Options: 'none' (default), 'slide', 'fade'.    'effect' => 'fade',    // Javascript event to trigger Ajax. Currently for: 'onchange'.    'event' => 'change',    'progress' => array(      // Graphic shown to indicate ajax. Options: 'throbber' (default), 'bar'.      'type' => 'throbber',      // Message to show along progress graphic. Default: 'Please wait...'.      'message' => NULL,    ),$form['random_user'] = array(  '#type' => 'button',  '#value' => 'Random Username',  '#ajax' => array(    'callback' => 'Drupal\ajax_example\Form\AjaxExampleForm::randomUsernameCallback',    'event' => 'click',    'progress' => array(      'type' => 'throbber',      'message' => 'Getting Random Username',    ),

Creating The Callbacks

After creating our form elements, it is time to create the callback functions which will return the response of what to update on the page.

These callbacks will return an instance of \Drupal\Core\Ajax\AjaxResponse. Each AjaxResponse instance will contain jQuery commands that will execute on the form. You can use the “addCommand()” method on AjaxResponse to add commands that implement \Drupal\Core\Ajax\CommandInterface.

Some commands such as CssCommand and ChangedCommand did not work. Thankfully, there is InvokeCommand which allows you to run any jQuery command. You can construct it with a jQuery selector, method, and arguments:

public InvokeCommand::__construct($selector, $method, array $arguments = array()) public InvokeCommand::__construct($selector, $method, array $arguments = array())

Here are the two callbacks for our form:

public function usernameValidateCallback(array &$form, FormStateInterface $form_state) { // Instantiate an AjaxResponse Object to return. $ajax_response = new AjaxResponse(); // Check if Username exists and is not Anonymous User (''). if (user_load_by_name($form_state->getValue('user_name')) && $form_state->getValue('user_name') != false) { $text = 'User Found'; $color = 'green'; } else { $text = 'No User Found'; $color = 'red'; } // Add a command to execute on form, jQuery .html() replaces content between tags. // In this case, we replace the desription with wheter the username was found or not. $ajax_response->addCommand(new HtmlCommand('#edit-user-name--description', $text)); // CssCommand did not work. //$ajax_response->addCommand(new CssCommand('#edit-user-name--description', array('color', $color))); // Add a command, InvokeCommand, which allows for custom jQuery commands. // In this case, we alter the color of the description. $ajax_response->addCommand(new InvokeCommand('#edit-user-name--description', 'css', array('color', $color))); // Return the AjaxResponse Object. return $ajax_response; } public function randomUsernameCallback(array &$form, FormStateInterface $form_state) { // Get all User Entities. $all_users = entity_load_multiple('user'); // Remove Anonymous User. array_shift($all_users); // Pick Random User. $random_user = $all_users[array_rand($all_users)]; // Instantiate an AjaxResponse Object to return. $ajax_response = new AjaxResponse(); // ValCommand does not exist, so we can use InvokeCommand. $ajax_response->addCommand(new InvokeCommand('#edit-user-name', 'val' , array($random_user->get('name')->getString()))); // ChangedCommand did not work. //$ajax_response->addCommand(new ChangedCommand('#edit-user-name', '#edit-user-name')); // We can still invoke the change command on #edit-user-name so it triggers Ajax on that element to validate username. $ajax_response->addCommand(new InvokeCommand('#edit-user-name', 'change')); // Return the AjaxResponse Object. return $ajax_response; } public function usernameValidateCallback(array &$form, FormStateInterface $form_state) {  // Instantiate an AjaxResponse Object to return.  $ajax_response = new AjaxResponse();  // Check if Username exists and is not Anonymous User ('').   if (user_load_by_name($form_state->getValue('user_name')) && $form_state->getValue('user_name') != false) {    $text = 'User Found';    $color = 'green';  } else {    $text = 'No User Found';    $color = 'red';  // Add a command to execute on form, jQuery .html() replaces content between tags.  // In this case, we replace the desription with wheter the username was found or not.  $ajax_response->addCommand(new HtmlCommand('#edit-user-name--description', $text));  // CssCommand did not work.  //$ajax_response->addCommand(new CssCommand('#edit-user-name--description', array('color', $color)));  // Add a command, InvokeCommand, which allows for custom jQuery commands.  // In this case, we alter the color of the description.  $ajax_response->addCommand(new InvokeCommand('#edit-user-name--description', 'css', array('color', $color)));  // Return the AjaxResponse Object.  return $ajax_response;public function randomUsernameCallback(array &$form, FormStateInterface $form_state) {  // Get all User Entities.  $all_users = entity_load_multiple('user');  // Remove Anonymous User.  array_shift($all_users);  // Pick Random User.  $random_user = $all_users[array_rand($all_users)];  // Instantiate an AjaxResponse Object to return.  $ajax_response = new AjaxResponse();  // ValCommand does not exist, so we can use InvokeCommand.  $ajax_response->addCommand(new InvokeCommand('#edit-user-name', 'val' , array($random_user->get('name')->getString())));  // ChangedCommand did not work.  //$ajax_response->addCommand(new ChangedCommand('#edit-user-name', '#edit-user-name'));  // We can still invoke the change command on #edit-user-name so it triggers Ajax on that element to validate username.  $ajax_response->addCommand(new InvokeCommand('#edit-user-name', 'change'));  // Return the AjaxResponse Object.  return $ajax_response;

Finished Form

Here is our finished Ajax Example Form:

Drupal Form Ajax Example

This blog post was created for Google Code-In 2014 to learn about a Drupal Core System.

Full Module Code

*/

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Jan 09 2015
Jan 09
[embedded content]

RDF UI is a module for Drupal 8 created by Sachini Aparna Herath for her Google Summer of Code 2014 project. RDF stands for Resource Description Framework; it provides a standardized model for data interchange. This module enables you to easily create mappings of Schema.org Things to Drupal Content Types and Fields. RDF UI will embed these specified mappings in the HTML as RDFa once your content is published. This blog post was made for Google Code-In 2014 to test and review RDF UI.

Installation

RDF UI can be easily installed using the Drush command line tool for Drupal. You may want to select installing the development version, as that will be the closest to running with the latest Drupal 8 development version:

drush dl rdfui --select drush dl rdfui --select

Upgrading

This section is about my experience upgrading RDF UI to the latest Drupal 8. This module was created over the summer, when GSoC takes place. As a result, it wasn’t compatible with the latest Drupal 8 development release, Beta 4.

I started upgrading from the stable version, and by the time I realized there was a development version, I had already done much of the upgrading, so I integrated the changes from that branch into my upgrade while still keeping most of mine similar to how it was before.

Some of the things I had to upgrade were: moving the attached CSS to a library, re-factoring the Form State from an array to an object, and accounting for the fact that Drupal\views_ui\OverviewBase was merged into Drupal\views_ui\DisplayOverviewBase. I also have included minor improvements here and there in the code.

One problem I encountered while using the sub module was that the default Schema.org/Text Data Type was text. However in the latest Drupal 8, this is the formatted text, and the plain text was needed, so this was fixed by changing the default from “text” to “string“:

diff --git a/rdf_builder/src/Form/ContentBuilderForm.php b/rdf_builder/src/Form/ContentBuilderForm.php index 6d2001a..c3cc50c 100644 --- a/rdf_builder/src/Form/ContentBuilderForm.php +++ b/rdf_builder/src/Form/ContentBuilderForm.php @@ -64,8 +64,8 @@ class ContentBuilderForm extends FormBase { public function __construct() { $this->converter = new SchemaOrgConverter(); $this->datatype_field_mappings = array( - 'http://schema.org/Text' => 'text', - 'http://schema.org/PostalAddress' => 'text_long', + 'http://schema.org/Text' => 'string', + 'http://schema.org/PostalAddress' => 'string_long', 'http://schema.org/Number' => 'integer', 'http://schema.org/MediaObject' => 'file', 'http://schema.org/AudioObject' => 'file', @@ -470,6 +468,6 @@ class ContentBuilderForm extends FormBase { return $this->datatype_field_mappings[$datatype]; } } - return 'text'; + return 'string'; } } diff --git a/rdf_builder/src/Form/ContentBuilderForm.php b/rdf_builder/src/Form/ContentBuilderForm.phpindex 6d2001a..c3cc50c 100644--- a/rdf_builder/src/Form/ContentBuilderForm.php+++ b/rdf_builder/src/Form/ContentBuilderForm.php@@ -64,8 +64,8 @@ class ContentBuilderForm extends FormBase {   public function __construct() {     $this->converter = new SchemaOrgConverter();     $this->datatype_field_mappings = array(-      'http://schema.org/Text' => 'text',-      'http://schema.org/PostalAddress' => 'text_long',+      'http://schema.org/Text' => 'string',+      'http://schema.org/PostalAddress' => 'string_long',       'http://schema.org/Number' => 'integer',       'http://schema.org/MediaObject' => 'file',       'http://schema.org/AudioObject' => 'file',@@ -470,6 +468,6 @@ class ContentBuilderForm extends FormBase {         return $this->datatype_field_mappings[$datatype];       }     }-    return 'text';+    return 'string';

The full upgrade patch can be viewed at this Issue on RDF UI.

Usage

As said on the project page, integration of Schema.org mappings in Content Types is seamless. In the “Add Content Type” form you can choose which Schema.org Type this Content Type will be:RDF UI Add Content Type

Once you fill this out and reach the “Manage Fields” page, you need to create you new fields. You can then click the “RDF Mappings” tab to assign these fields their Schema.org property:

RDF UI Manage Fields

Now you are ready to go ahead and create your content. Once created and published, the node should show the fields and the html should contain the Schema.org Type in the article tag and Schema.org Properties in the field-items divisions:

RDF UI Awesome Event

RDF UI Builder!

RDF UI also comes with a very helpful sub module named: RDF UI Builder. Want to shorten up all the steps above in creating Schema.org mapped Content Types? This sub module comes in handy for that very purpose.

Once this module is enabled, you can find the new “+ Add Schema.org Content Type” button next to the original “+ Add content type” button:

RDF UI Add Schema.org Content Type

After selecting which Schema.org Type you want to use, you are redirected to the next page where the only thing you need to do is select which fields you want, and they will automatically be created and mapped for you!

RDF UI Choose Fields RDF UI Content Type Event Created

That’s it, now you can go off and create content for that type.

Conclusion

Sachini Aparna Herath, with the help of her mentors Stéphane Corlosquet and Kevin Oleary, has created a great module for Drupal 8. RDF UI fits in with the rest of Drupal, and can be used to quickly create content types or fields and assign them Schema.org Types and Properties. This can help any site owner to provide “semantic rich data” on their web pages.

One improvement I can suggest is that the http://schema.org/Date Type should default as date only in Drupal. As of right now both http://schema.org/DateTime and http://schema.org/Date convert into Drupal datetime. This may be because Drupal does not have Date and DateTime options in the drop down. If this is the case, this improvement would be for Drupal Core to move selecting DateTime or only Date to the main select menu before selecting Date and then choosing for a new field.

From a Google Code-In perspective, this task had many obstacles which I had to go through to upgrade the module to be working with the latest Drupal 8 version, Beta 4, and I am glad as I keep learning more with the more problems I face. It is also the first time I have created a change record, because one of the errors received had not been included in the list of changes.

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Dec 31 2014
Dec 31
[embedded content]

As your website grows, there will be a point when there are more people accessing your web server than possible for a single server to handle. This is when load balancing will become a critical step in your Drupal setup. Load balancing increases reliability of your application in case a web server goes down and spreads the load across multiple web servers. In this tutorial, we are going to use HAProxy as a Layer 4 Load Balancer for our Drupal website. We will have a proxy server, two web servers, and one database server, all running on Ubuntu 14.04 LTS (Trusty Tahr) 32-bit.

Load Balancing Diagram 2

The public will be able to access the proxy server; this server will then use an algorithm to redirect the user to a web server which will access the database server if needed and respond with generated web page or content requested.

In a production environment, there would be separate physical servers for each proxy, web, and database. However, for the sake of simplicity and availability, I will be using virtual machines.

Setting Up Vagrant

Vagrant is an easy tool you can use to quickly “create and configure lightweight, reproducible, and portable development environments.” Along with this, you will need to install VirtualBox to emulate these virtual machines.

Go ahead and create a folder for our Vagrant environment. For example, I will create mine at: C:\Users\{user-name}\Documents\Vagrant\drupal. Note: this directory will not contain any Drupal files.

Open up command prompt and run this command to download the Ubuntu 14.04 32-bit box:

vagrant box add ubuntu/trusty32 vagrant box add ubuntu/trusty32

This will download the pre-configured box for VirtualBox which will reduce the time required to create our later virtual machines.

In this directory, lets initialize Vagrant. To do this, run:

vagrant init ubuntu/trusty32 --minimal vagrant init ubuntu/trusty32 --minimal

Load Balancing Vagrant Init

This will create a configuration file called “Vagrantfile” in that directory. This file will define basic settings for our virtual machines such as box, hostname, and public/private IP. Throughout this tutorial, we will keep adding configurations for the different servers we are going to set up.

Setting Up Web Server 1

Lets get started with this Load Balancing Setup, but starting with both of our web servers. These web servers will be the actual computers processing the web page, while the proxy server will just forward the requests.

To start, lets edit our Vagrantfile to look like this:

Vagrant.configure(2) do |config| # Sets up all virtual machines using this box. config.vm.box = "ubuntu/trusty32" # Agent forwarding over SSH connections is enabled config.ssh.forward_agent = true config.vm.define "web1" do |web1| # Sets hostname of server. config.vm.hostname = "web1" # Allows access to server from host machine. # 127.0.0.1:8884 (Your computer) ---> 192.168.50.4:80 (Server) config.vm.network :forwarded_port, guest: 80, host: 8884 # Sets IP for private network with other servers. config.vm.network "private_network", ip: "192.168.50.4", # Set name of the internal network. virtualbox__intnet: "intnet" end end Vagrant.configure(2) do |config|  # Sets up all virtual machines using this box.  config.vm.box = "ubuntu/trusty32"  # Agent forwarding over SSH connections is enabled  config.ssh.forward_agent = true  config.vm.define "web1" do |web1|      # Sets hostname of server.      config.vm.hostname = "web1"   # Allows access to server from host machine.   # 127.0.0.1:8884 (Your computer) ---> 192.168.50.4:80 (Server)   config.vm.network :forwarded_port, guest: 80, host: 8884   # Sets IP for private network with other servers.      config.vm.network "private_network", ip: "192.168.50.4",   # Set name of the internal network.      virtualbox__intnet: "intnet"

We will set the option to set up all our virtual machines as Ubuntu 14.04. We will give this specific virtual machine, a host name of web1. Forwarding ports allows a machine only connected to a private network, and not a public network to be accessed by the host computer. So for example, we set up the port 8884 to redirect to port 80 on the guest. As a result, if we visit 127.0.0.1:8884 on our host computer, it will be forwarded to port 80 of that virtual machine. We have set the private IP as 192.168.50.4. This is the type of IP that web1 and all other servers to be set up will use to communicate with each other. We have set the internal network name to “intnet” which we will need to keep constant throughout all servers.

Once you have saved this, go ahead and start up this server using this command:

vagrant up web1 vagrant up web1

This will import the Ubuntu box, and configure the server according to our settings in Vagrantfile. This will take longer the first time since it has to get everything set up.

Load Balancing Vagrant Up Web1

Once the virtual machine has booted up you can ssh into it using:

vagrant ssh web1 vagrant ssh web1

Load Balancing Vagrant SSH Web1

Installing Dependencies

Then lets update the package list and install the required software:

sudo apt-get update sudo apt-get install nginx php5-fpm php5-mysql php5-gd sudo apt-get updatesudo apt-get install nginx php5-fpm php5-mysql php5-gd

Before we start with downloading and running Drupal, we need to first configure PHP and Nginx.

Configuring PHP

Lets start with editing the configuration of php5-fpm:

sudo nano /etc/php5/fpm/php.ini sudo nano /etc/php5/fpm/php.ini

In this file, find the option cgi.fix_pathinfo. By default, this value is set to 1. However, this is a security risk since you can access a file by typing in something that is close to it. Uncomment and change the value to 0:

cgi.fix_pathinfo=0 cgi.fix_pathinfo=0

Now you can save and exit out of this file since we are done with it.

Next, lets edit pool.d/www.conf:

sudo nano /etc/php5/fpm/pool.d/www.conf sudo nano /etc/php5/fpm/pool.d/www.conf

Here, if not set to this value already, change the listen value to the php5-fpm unix domain socket:

listen = /var/run/php5-fpm.sock listen = /var/run/php5-fpm.sock

Save and exit. Now lets restart the php5-fpm process for these changes to take effect:

sudo service php5-fpm restart sudo service php5-fpm restart

Configuring Nginx

It is time to configure our web server, Nginx. To start, lets make a copy of the default site config in the available sites. Then, lets edit it:

sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/drupal sudo nano /etc/nginx/sites-available/drupal sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/drupalsudo nano /etc/nginx/sites-available/drupal

Now make changes accordingly so your drupal virtual hosts file looks like this in the server block(Note: only replace corresponding lines with lines below, do not simply copy and paste the code):

server { # Listen for requests in port 80. listen 80; # Comment out IPv6 line. #listen [::]:80 default_server ipv6only=on; # Root of where our Drupal files will be located. root /var/www/drupal; # Order in which to look for index files. index index.php index.html index.htm; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; # Uncomment to enable naxsi on this location # include /etc/nginx/naxsi.rules } # redirect server not found pages to /404.html error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini try_files $uri =404; # With php5-cgi alone: #fastcgi_pass 127.0.0.1:9000; # With php5-fpm: fastcgi_pass unix:/var/run/php5-fpm.sock; fastcgi_index index.php; include fastcgi_params; } } # Listen for requests in port 80. listen 80; # Comment out IPv6 line. #listen [::]:80 default_server ipv6only=on; # Root of where our Drupal files will be located. root /var/www/drupal; # Order in which to look for index files. index index.php index.html index.htm; location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; # Uncomment to enable naxsi on this location # include /etc/nginx/naxsi.rules # redirect server not found pages to /404.html error_page 404 /404.html; # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini try_files $uri =404; # With php5-cgi alone: #fastcgi_pass 127.0.0.1:9000; # With php5-fpm: fastcgi_pass unix:/var/run/php5-fpm.sock; fastcgi_index index.php; include fastcgi_params;

This will listen to all requests coming from port 80. We will also comment out the IPv6 line. Change the root to where you will have your Drupal files. I have set my root to /var/www/drupal. Uncomment the 404 and 50x error lines. Comment out the line under “# With php5-cgi alone” since we have installed php5-fpm. Add the line above “# With php5-cgi alone” in the example to immediately return a 404 if an exact match is not found.

Once you have completed this, lets remove the default virtual host from the enabled sites, and make a symbolic link of drupal to the enabled sites from the available sites:

sudo rm /etc/nginx/sites-enabled/default sudo ln -s /etc/nginx/sites-available/drupal /etc/nginx/sites-enabled/ sudo rm /etc/nginx/sites-enabled/defaultsudo ln -s /etc/nginx/sites-available/drupal /etc/nginx/sites-enabled/

Now that we have configured everything, lets restart the nginx service:

sudo service nginx restart sudo service nginx restart

Preparing Drupal

Lets go into our home directory and download the latest version of Drupal. I will be downloading 8.0.0-beta4. You can find the latest Drupal releases here.

cd ~ wget http://ftp.drupal.org/files/projects/drupal-8.0.0-beta4.tar.gz wget http://ftp.drupal.org/files/projects/drupal-8.0.0-beta4.tar.gz

Once this is finished downloading, we can extract the contents:

tar xzvf drupal-8.0.0-beta4.tar.gz tar xzvf drupal-8.0.0-beta4.tar.gz

After the contents are extracted, we can now create the directory we defined as root in nginx. Lets create this folder and copy all the Drupal files to there:

sudo mkdir -p /var/www/drupal sudo cp -r ~/drupal-8.0.0-beta4/* /var/www/drupal sudo mkdir -p /var/www/drupalsudo cp -r ~/drupal-8.0.0-beta4/* /var/www/drupal

Setting Permissions

Lets set the owner of all these files to our user account, in this case “vagrant”, and change the group to “www-data”, the web server user:

sudo chown -R vagrant:www-data /var/www/drupal sudo chown -R vagrant:www-data /var/www/drupal

Now lets also add our user to the www-data group, and give that group permission to read and write to all files:

sudo usermod -a -G www-data vagrant sudo chmod -R g+rw /var/www/drupal sudo usermod -a -G www-data vagrantsudo chmod -R g+rw /var/www/drupal

You should now be able to access the Drupal installation page by visiting 127.0.0.1:8884 which we set up in the port forwarding setting in the Vagrantfile. However, don’t install Drupal just yet! We still need to sync up both web servers.

Setting Up Web Server 2

There are two ways you can go about creating web server 2. You can either repeat the process of setting up web server 1 or you can create a clone of web server 1 and have vagrant set up the replica. I will be going with the latter.

First, lets create a replica box. To do this, halt your first webserver:

vagrant halt web1 vagrant halt web1

Then, lets create a package of that virtual machine:

vagrant package web1 vagrant package web1

This command will package everything in web1 into your Vagrant directory as package.box.

Load Balancing Vagrant Package Web1

Now, in your Vagrantfile, go ahead and add the configuration for web2:

config.vm.define "web2" do |web2| # Custom box name config.vm.box = "drupal-nginx-php" # Location of packaged box config.vm.box_url = "file:///C:/Users/{user-name}/Documents/Vagrant/drupal/package.box" config.vm.hostname = "web2" config.vm.network :forwarded_port, guest: 80, host: 8886 config.vm.network "private_network", ip: "192.168.50.5", virtualbox__intnet: "intnet" end   config.vm.define "web2" do |web2|   # Custom box name      config.vm.box = "drupal-nginx-php"   # Location of packaged box   config.vm.box_url = "file:///C:/Users/{user-name}/Documents/Vagrant/drupal/package.box"      config.vm.hostname = "web2"   config.vm.network :forwarded_port, guest: 80, host: 8886      config.vm.network "private_network", ip: "192.168.50.5",      virtualbox__intnet: "intnet"

You can give the box we just packaged a name like “drupal-nginx-php”. Set “config.vm.box_url” as the location of the package.box. We will be giving this machine a hostname of web2, port forwarding HTTP IP of 8886, and a private network IP of 192.168.50.5.

Now, lets start both of our servers again:

vagrant up web1 web2 vagrant up web1 web2

Synchronizing Drupal Between Web Servers

We need to synchronize the Drupal folder between both servers so if an image or module gets uploaded on one server, it will not be broken if another user connecting to the other servers is looking for that content.

There are many solutions to syncing file on two different servers such as GlusterFS, and NFS. While using GlusterFS, the core directory in Drupal always replied with a Input/output error. As a result, I will be using Unison to sync files across web1 and web2. As an alternative you can also use a script I wrote that utilize inotifywait and scp to sync files.

The benefit of using inotifywait with scp is that it will only sync whenever there is a file modified, created, deleted, or moved. However, this option is unidirectional, so you will need to run it on both servers. On the other hand, Unison is bidirectional and can run on one server only.

Syncing with Unison

Start by installing unison on both machines:

sudo apt-get install unison sudo apt-get install unison

Once installed, run the command which will create the folder “~/.unison”:

Now, on web1, lets make a copy the default profile and edit our drupal profile:

cp ~/.unison/default.prf ~/.unison/drupal.prf nano ~/.unison/drupal.prf cp ~/.unison/default.prf ~/.unison/drupal.prfnano ~/.unison/drupal.prf

Make your drupal profile for Unison look like this:

# Folders to sync root = /var/www/drupal root = ssh://[email protected]//var/www/drupal # batch mode: ask no questions at all batch = true # automatically accept default (nonconflicting) actions auto = true # synchronize modification times times = true # synchronize owner owner = true # synchronize group attributes group = true # Folders to syncroot = /var/www/drupal# batch mode: ask no questions at allbatch = true# automatically accept default (nonconflicting) actionsauto = true# synchronize modification timestimes = true# synchronize ownerowner = true# synchronize group attributesgroup = true

Save and exit. You can find more settings in the Unison User Manual.

You only need one server running Unison since it syncs bidirectionally, so you only need to create this profile file on one machine. The root variables in this file will tell unison which folders to sync. Since the second folder to sync is on another server, we give it a absolute folder via ssh.

To sync both files you can run:

unison drupal unison drupal

The first time you run this, you might get a prompt asking you whether you want to add web2’s ECDSA key fingerprint to your known hosts. Type “yes”. It will also for the first time create archives of all the files which will take some time depending on the amount of files you have. For Drupal 8, it should take a minute or less.

Load Balancing Unison Drupal

However, there is a problem. This will only sync once and exit. We want unison to keep syncing the files. For this, we can use the -repeat flag.

Still there is an issue if we just run this from our SSH client. First, once we start Unison with repeat, it will start, but we cannot run any other command without stopping Unison. Second, we need to keep our terminal open at all times. If we close our client, a HUP or hangup will be sent to the server, and Unison will be stopped.

The Solution? We can use screen. We can start unison in screen and detach from it. Even if we exit our client, we can go back into the screen and see output from Unison.

If you do not already have screen installed:

sudo apt-get install screen sudo apt-get install screen

Next you can start a new screen by typing:

It will look like your screen was cleared. Now lets starts unison with repeat mode set to watch, or whenever a file is updated:

unison drupal -repeat watch unison drupal -repeat watch

Load Balancing Unison Drupal Repeat Watch

If repeat by watch does not work, you can set a value for how many seconds it should wait to sync again:

unison drupal -repeat 1 unison drupal -repeat 1

Load Balancing Unison Drupal Repeat 1

The above will sync the folders every second. You should keep it low when configuring Drupal, however once you have set up Drupal and don’t need constant syncing, you can increase the amount of time between each sync, set a cron job, or manually run the command whenever you update one server.

You can disconnect from the screen by keying Ctrl + A + D. This will keep the process running. If you ever want to reattach to the screen, get the screen id from the list:

screen -list screen -list

Then, use the beginning characters of the screen to reattach:

screen -r {id} screen -r {id}

Note: If you only have one screen open, using -r will automatically reattach you to that screen.

Syncing with drupal_sync (inotifywait + scp)

If you want to use my Bash script you will first need to install inotify-tools:

sudo apt-get install inotify-tools sudo apt-get install inotify-tools

Once you have done this, transfer this file over to your home (~) directory through SFTP or another method:

#!/bin/bash SYNC_DIR="/var/www/drupal" REMOTE_USER="vagrant" REMOTE_HOST="192.168.50.5" LAST_MODIFY_TIME=0 do_start() { inotifywait -mr $SYNC_DIR --timefmt '%Y%m%d%H%M%S' --format '%T %e %w%f' -e create,modify,attrib,move,delete | while read time event file; do case $event in CREATE|MODIFY|MOVE_TO|CREATE,ISDIR) if [ $event = MODIFY ]; then if [ $LAST_MODIFY_TIME -ne 0 ]; then if [ $(($time - $LAST_MODIFY_TIME)) -lt 5 ]; then continue; fi; fi LAST_MODIFY_TIME=$time; if [ "$(ssh [email protected]$REMOTE_HOST 'cat $file' | diff - $file)" >/dev/null ]; then continue fi fi if [ $event = CREATE,ISDIR ]; then exists=$(ssh [email protected]$REMOTE_HOST "[ -d $file ] && echo 1 || echo 2") if [ $exists = 1 ]; then continue fi fi scp -rp $file [email protected]$REMOTE_HOST:$file ;; ATTRIB|ATTRIB,ISDIR) perm=$(stat -c "%U %G" $file) permr=$(ssh [email protected]$REMOTE_HOST "stat -c '%U %G' $file") if [ "$perm" != "$permr" ]; then echo $perm | while read owner group; do ssh [email protected]$REMOTE_HOST "chown $owner:$group $file"; done fi ;; MOVE_FROM|DELETE|DELETE,ISDIR) exists=$(ssh [email protected]$REMOTE_HOST "test -e $file && echo 1 || echo 0") if [ $exists = 1 ]; then ssh [email protected]$REMOTE_HOST "rm -r $file" fi ;; esac done & echo "Drupal Sync Started" } do_stop() { if [ -z "$(pgrep inotifywait)" ]; then echo "Drupal Sync Process Never Started" else pkill inotifywait echo "Drupal Sync Process Killed" fi } show_status() { if [ -z "$(pgrep inotifywait)" ]; then echo "Drupal Sync is Not Running" else echo "Drupal Sync is Running" fi } case "$1" in start) do_start ;; stop) do_stop ;; status) show_status ;; restart|force-reload) do_stop do_start ;; *) echo "Usage: drupal_sync {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac #!/bin/bashSYNC_DIR="/var/www/drupal"REMOTE_USER="vagrant"REMOTE_HOST="192.168.50.5"LAST_MODIFY_TIME=0do_start() inotifywait -mr $SYNC_DIR --timefmt '%Y%m%d%H%M%S' --format '%T %e %w%f' -e create,modify,attrib,move,delete | while read time event file; do case $event in CREATE|MODIFY|MOVE_TO|CREATE,ISDIR) if [ $event = MODIFY ]; then if [ $LAST_MODIFY_TIME -ne 0 ]; then if [ $(($time - $LAST_MODIFY_TIME)) -lt 5 ]; then continue; fi; LAST_MODIFY_TIME=$time; if [ "$(ssh [email protected]$REMOTE_HOST 'cat $file' | diff - $file)" >/dev/null ]; then if [ $event = CREATE,ISDIR ]; then exists=$(ssh $REMOTE_USER@$REMOTE_HOST "[ -d $file ] && echo 1 || echo 2") if [ $exists = 1 ]; then scp -rp $file $REMOTE_USER@$REMOTE_HOST:$file ATTRIB|ATTRIB,ISDIR) perm=$(stat -c "%U %G" $file) permr=$(ssh $REMOTE_USER@$REMOTE_HOST "stat -c '%U %G' $file") if [ "$perm" != "$permr" ]; then echo $perm | while read owner group; do ssh $REMOTE_USER@$REMOTE_HOST "chown $owner:$group $file"; done MOVE_FROM|DELETE|DELETE,ISDIR) exists=$(ssh $REMOTE_USER@$REMOTE_HOST "test -e $file && echo 1 || echo 0") if [ $exists = 1 ]; then ssh $REMOTE_USER@$REMOTE_HOST "rm -r $file" echo "Drupal Sync Started" if [ -z "$(pgrep inotifywait)" ]; then echo "Drupal Sync Process Never Started" pkill inotifywait echo "Drupal Sync Process Killed"show_status() if [ -z "$(pgrep inotifywait)" ]; then echo "Drupal Sync is Not Running" echo "Drupal Sync is Running"case "$1" in  start)  status) show_status  restart|force-reload) echo "Usage: drupal_sync {start|stop|status|restart|force-reload}" >&2

Edit the variables at the top of the script to your server settings.

Before we can run this script, we need to add permissions to execute this file:

chmod +x ~/drupal_sync chmod +x ~/drupal_sync

Now transfer it over to your other server with scp:

scp ~/drupal_sync [email protected]:~/drupal_sync scp ~/drupal_sync vagrant@192.168.50.5:~/drupal_sync

Note: you will also need to edit the variabled at the top of the script on the other server, web2, so that the “$REMOTE_HOST” is set to the private IP of web1.

We can run the file, however every time it will prompt you for the other server’s ssh password. To allow automatic access we need to make a pair of SSH keys:

ssh-keygen -t rsa ssh-keygen -t rsa

Do not give a name, or pass-phrase, just press enter.

Once you have generated a pair of RSA keys, we need to add the public key to the authorized keys of the other server:

cat ~/.ssh/id_rsa.pub | ssh [email protected] 'cat >> ~/.ssh/authorized_keys' cat ~/.ssh/id_rsa.pub | ssh vagrant@192.168.50.5 'cat >> ~/.ssh/authorized_keys'

Now, after you complete this command by typing in the password of [email protected] again, the next time you ssh you should automatically be connected:

ssh [email protected] ssh vagrant@192.168.50.5

If you are taken to a new SSH screen, congratulations it has worked, to exit back into your other server:

Now repeat this process of generating RSA keys from web2 to web1, so web2 can access web1 without being prompted for a password.

Once both of your servers can SSH to each other automatically, you can start the drupal sync script on both servers:

~/drupal_sync start ~/drupal_sync start

Setting Up The Database Server

Now that we have successfully set up and synced both of our web servers, web1 and web2, it is time to set up the database server that is going to be accessed by both.

Start by adding the virtual machine settings in Vagrantfile:

config.vm.define "db" do |db| config.vm.hostname = "db" config.vm.network "private_network", ip: "192.168.50.6", virtualbox__intnet: "intnet" end   config.vm.define "db" do |db|      config.vm.hostname = "db"      config.vm.network "private_network", ip: "192.168.50.6",      virtualbox__intnet: "intnet"

We will give this server a host name of db and a private IP of 192.168.50.6.

Lets get the database server up and running:

vagrant up db vagrant up db

Installing MySQL

Once the virtual machine has booted, lets ssh into the server, update package list, and install MySQL server:

vagrant ssh db sudo apt-get update sudo apt-get install mysql-server vagrant ssh dbsudo apt-get updatesudo apt-get install mysql-server

While this is installing, it will ask you to make and confirm your root user password, make this complex!

Load Balancing Database Root Password

Once this has finished install, we need to run the actual MySQL installation:

sudo mysql_install_db sudo mysql_install_db

By default, many things will be included in this install for testing. To be more secure, run this command:

sudo mysql_secure_installation sudo mysql_secure_installation

Load Balancing Database Secure Installation

Configuring MySQL

As of right now, MySQL will only accept connections coming from the same computer, so we need to allow for it to accept connections from outside by enabling remote access.

Lets edit the MySQL configuration file:

sudo nano /etc/mysql/my.cnf sudo nano /etc/mysql/my.cnf

Under the section [mysqld] change the value of bind-address from localhost to the private IP of the db server.

bind-address = 192.168.50.6 bind-address        = 192.168.50.6

When you are finished, save and close out of the editor. Next, lets restart the MySQL server for the changes to take effect:

sudo service mysql restart sudo service mysql restart

Creating MySQL Database & Users

Now that we have MySQL listening to a private IP that our web servers can access, we need to create MySQL users for our web servers.

Log in to MySQL as the root user with the password we set earlier:

mysql -u root -p mysql -u root -p

First, lets create our drupal database:

CREATE DATABASE drupal CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE DATABASE drupal CHARACTER SET utf8 COLLATE utf8_general_ci;

Now lets create users for both web servers:

CREATE USER 'drupal_user'@'192.168.50.4' IDENTIFIED BY 'password'; CREATE USER 'drupal_user'@'192.168.50.5' IDENTIFIED BY 'password'; CREATE USER 'drupal_user'@'192.168.50.4' IDENTIFIED BY 'password';CREATE USER 'drupal_user'@'192.168.50.5' IDENTIFIED BY 'password';

The IP following the name of your user should be the private IP’s of your web servers. I have set the password as “password” for both my users, however you should change it to something much more secure.

The last thing we need to do is grant privileges to these users at their respective IP address. For Drupal, you need to give these privileges to your users:

GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES ON drupal.* TO 'drupal_user'@'192.168.50.4'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES ON drupal.* TO 'drupal_user'@'102.168.50.5'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES ON drupal.* TO 'drupal_user'@'192.168.50.4';GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, CREATE TEMPORARY TABLES ON drupal.* TO 'drupal_user'@'102.168.50.5';

Once done, flush privileges and exit:

FLUSH PRIVILEGES; exit FLUSH PRIVILEGES;

Our database server, db, is now configured and ready to be used by web1 and web2.

Setting Up The Proxy / Load Balancing Server

The only server left to set up is the load balancing server which acts as a proxy to both web servers. This may be the last to be set up, but certainly the most critical in this setup.

Lets add the server config to our Vagrantfile:

config.vm.define "balancer" do |balancer| config.vm.hostname = "balancer" config.vm.network :forwarded_port, guest: 80, host: 9090 # We want to be assinged a public IP config.vm.network "public_network" config.vm.network "private_network", ip: "192.168.50.7", virtualbox__intnet: "intnet" end   config.vm.define "balancer" do |balancer|      config.vm.hostname = "balancer"      config.vm.network :forwarded_port, guest: 80, host: 9090      # We want to be assinged a public IP      config.vm.network "public_network"      config.vm.network "private_network", ip: "192.168.50.7",      virtualbox__intnet: "intnet"

This server will have a host name of balancer. We have also declared a private IP of 192.168.50.6 and forward port 80 to port 9090 in our local machine. Once important different between the rest of the other server and this server is the fact we set in the configuration that we want to be connected to the public network. This will automatically assign and public IP to the server. We can then access this server at that IP from our host machine.

Lets start up the balancer server:

vagrant up balancer vagrant up balancer

Since we have set this server to connect to the public network, it might ask you to choose an adapter. Try the first one, if it doesn’t work, halt and up balacer again, then choose the other option/s. For me, my Wi-Fi Adapter worked.

Finding the Public IP

Once the server has booted up, notice the order of your network adapters, it should look something like this:

==> balancer: Preparing network interfaces based on configuration... balancer: Adapter 1: nat balancer: Adapter 2: bridged balancer: Adapter 3: intnet ==> balancer: Preparing network interfaces based on configuration...    balancer: Adapter 1: nat    balancer: Adapter 2: bridged    balancer: Adapter 3: intnet

Find where the “bridged” adapter is. Mine is set as bridged for Adapter 2.

Next, lets ssh into the balancer server.

vagrant ssh balancer vagrant ssh balancer

To get our IP configuration, lets run this command:

Load Balancing ifconnfig

Since my second adapter was the bridged network, I will look at the second link: eth1 for my IP address. It will be printed after “inet addr”. My public IP for the balancer is 192.168.1.110. This will be the IP we will use to connect to the balancer/proxy in our browser.

Installing HAProxy

Now, lets update package list, and install haproxy:

sudo apt-get update sudo apt-get install haproxy sudo apt-get updatesudo apt-get install haproxy

Configuring HAProxy

Lets make it so that HAProxy will start on every boot of the server:

sudo nano /etc/default/haproxy sudo nano /etc/default/haproxy

In this file, change the value of “ENABLED” to 1.

Save this file and exit.

Now, it’s time to set up the main HAProxy configuration:

sudo nano /etc/haproxy/haproxy.cfg sudo nano /etc/haproxy/haproxy.cfg

We will let the global settings stay the same. Under “defaults”, change the mode and option from “http” to “tcp”:

defaults ... mode tcp option tcplog ... mode    tcp option  tcplog

Replacing “http” with “tcp” will tell HAProxy that we are going to be using a Layer 4 Load Balancing.

Next, we need to make a frontend, the balancer, with the address to listen, and which backend to point to. The frontend is the server the public can access, and the backend are servers that cannot be directly accessed, like web1 and web2. Add the www frontend:

frontend www bind 192.168.1.110:80 default_backend drupal-backend frontend www bind 192.168.1.110:80 default_backend drupal-backend

You want to bind the public IP with port 80. So, whenever someone requests at port 80 to that public IP, it will use the default backend, “drupal-backend”. Now, lets define “drupal-backend”:

backend drupal-backend balance roundrobin mode tcp server webserver1 192.168.50.4:80 check server webserver2 192.168.50.5:80 check backend drupal-backend balance roundrobin server webserver1 192.168.50.4:80 check server webserver2 192.168.50.5:80 check

Here, we have defined that we want to use the round robin algorithm to chose a server for the request. There are many algorithms you can choose from. Here are a few:

  • roundrobin – “Each server is used in turns, according to their weights.”
  • leastconn – “The server with the lowest number of connections receives the connection.”
  • source – “The source IP address is hashed and divided by the total weight of the running servers to designate which server will receive the request. This ensures that the same client IP address will always reach the same server as long as no server goes down or up.”
  • uri – “This algorithm hashes either the left part of the URI (before the question mark) or the whole URI (if the “whole” parameter is present) and divides the hash value by the total weight of the running servers. … This ensures that the same URI will always be directed to the same server as long as no server goes up or down.”

You can find more algorithms in the HAProxy Configuration Manual.

Again, the mode as “tcp” will specify we want Layer 4 and not Layer 7 Load Balancing. We also define our two servers with names “webserver1” and “webserver2”. The check is added at the end to check the health of the server before sending the request there. If one server fails, the other server will be sent all the requests.

We are done configuring HAProxy, so lets save and exit.

HAProxy Logging

If you want to enable logging, you need to edit /etc/rsyslog.conf:

sudo nano /etc/rsyslog.conf sudo nano /etc/rsyslog.conf

Find and uncomment the first two lines, then add the third line:

$ModLoad imudp $UDPServerRun 514 $UDPServerAddress 127.0.0.1 $ModLoad imudp$UDPServerRun 514$UDPServerAddress 127.0.0.1

Logging will now be enabled, and HAProxy logs can be viewed at: /var/log/haproxy.log once started.

When you are done editing this file, save and exit. Now, restart both rsyslog and haproxy services:

sudo service rsyslog restart sudo service haproxy restart sudo service rsyslog restartsudo service haproxy restart

Installing Drupal

We are finally at the last step of getting Drupal working on a Load Balanced setup. It is time to install Drupal.

You should now be able to see the Drupal installation page when you visit the public IP of your load balancing server from your host machine.

Load Balancing Drupal Installation

Once you click “Save and continue” on the Profile selection page, settings.php and services.yml should appear in /var/www/drupal/sites/default/.

Load Balancing Drupal Sites Default Permissions

By default, Drupal will not be able to edit these as necessary because it does not have permission to write to these files as it’s user www-data. Since we set the group permissions to www-data, we need to give the group write permissions on these files:

sudo chmod g+w /var/www/drupal/sites/default/settings.php sudo chmod g+w /var/www/drupal/sites/default/services.yml sudo chmod g+w /var/www/drupal/sites/default/settings.phpsudo chmod g+w /var/www/drupal/sites/default/services.yml

Once done with the process, it is recommended you remove write permissions to keep your site secure:

sudo chmod g-w /var/www/drupal/sites/default/settings.php sudo chmod g-w /var/www/drupal/sites/default/services.yml sudo chmod g-w /var/www/drupal/sites/default/settings.phpsudo chmod g-w /var/www/drupal/sites/default/services.yml

You can now continue with the installation. Once the database options come up, you need to specify the private IP, database, user, and password we made during the MySQL Database server setup.

Load Balancing Drupal Database Configuration

Again, follow the configuration of Drupal, and you should soon come to your new Drupal install on a Layer 4 Load Balanced Setup using HAProxy.

Load Balancing Drupal Home Page

Conclusion

There you go, we now have a fully functional Layer 4 Load Balacing Setup using 4 servers running Drupal. As your traffic increases, this setup will reduce load on your servers by spreading your users out to different servers. It can also be fail-safe. If one day you wake up to find one of your servers down, throughout the night all your traffic was being sent to the other available server/s, so your users wouldn’t have experienced any downtime with your website.

To test if the load balancing is working and/or with your algorithm of choice, you can view the logs we enabled during the HAProxy server setup at /var/log/haproxy.log.

This blog post was made for Google Code-In 2014. This task has by far been the most challenging, and I have truly expanded my knowledge on Linux servers and Unix commands.

Troubleshooting

If you try running the command vagrant, and you get a response that says the command could not be found, try appending the directory of where vagrant.exe exists to the “PATH” variable in your System/User Environment Variables.

If you are having trouble when running vagrant ssh, this is because there is no ssh command on Windows by default. Luckily, if you have git, you can add the {git-directory}/bin to your System/User Evironment Variables to get access to many commands such as ssh, and ls on Windows. if you do no have git installed, you can use PuTTY to SSH to your server at 127.0.0.1:2200, or the assigned forwarded port for SSH.

If you get 50x errors while visiting your HTTP forwarded ports on web1 or web2, and have this in your nginx logs: “*1 connect() to unix:/var/run/php5-fpm.sock failed (13: Permission denied)”, you need to edit /etc/php5/fpm/pool.d/www.conf and uncomment these lines:

listen.owner = www-data listen.group = www-data listen.mode = 0660 listen.owner = www-datalisten.group = www-datalisten.mode = 0660

If you get PHP Fatat errors such as: “Unexpected [, expecting )”, this is because you have installed a version of php5-fpm below PHP 5.4. This error means your PHP version does not support short-hand array notation in PHP. Solution is to find a PPA to update your PHP version to 5.4+ or update to Ubuntu 14.04 which should have PHP 5.4+ with php5-fpm.

Sources

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Dec 25 2014
Dec 25
[embedded content]

FluxPocket is a module created by Umar Ahmad for the FluxKraft distribution of Drupal. FluxKraft is an easy tool you can use for self-hosted, social automation. However, it does not support Pocket by default.  As a result, Umar Ahmad made the FluxKraft module as part of his Google Summer of Code 2014 project. This blog post was made for Google Code-In 2014 to test and review FluxPocket.

Installation

The installation process is very simple. The module can be cloned and enabled with no errors. The instructions are clearly written in the USAGE.md file. The most complicated part of the installation can be attributed to running commands:

drush composer-json-rebuild drush composer-manager update drush composer-json-rebuilddrush composer-manager update

FluxPocket Installation

If the second command’s output is “Killed“, you can restart your Apache/PHP server to fix this and finish installing all dependencies.

Configuration

All Pocket integration is nicely fitted in with the other services available in FluxKraft such as Twitter, Facebook, Flickr, and DropBox. You can easily follow FluxKraft or FluxPocket instructions to install and configure Pocket service such as creating a new Pocket app, and enabling access to the app from your Pocket account. However, one problem is after adding the service account under Configuration -> Web services -> Service accounts -> Add account, the pocket account needs to be added again by clicking Service accounts (at top right of screen) -> Add account. Other than this, configuration is very smooth.

FluxPocket Configuration

Usage

Adding new rules to FluxKraft with FluxPocket is seamless. Many events and actions are detailed in the GitHub repo, and all seem to work great! Examples of events are: if new URL is added to Pocket, or if URL is added to favorites in Pocket. Examples of actions are: Add URL to Pocket, Archive URL in Pocket, Add/Replace/Remove tags of a URL in Pocket.

FluxPocket Create New Article

FluxPocket Pocket Article

Conclusion

Lastly, it can be said that the FluxPocket module is a solid addition to the FluxKraft distribution of Drupal. Events and actions that are provided by this module work great in integrating Pocket with the other services available by default.

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Dec 24 2014
Dec 24
[embedded content]

Drupal faced one of its biggest security vulnerabilities recently. It was so bad, it was dubbed “Drupalgeddon”. It affected every single site that was running Drupal 7.31 (latest at the time) or below, as you can read in this Security Advisory.

The exploit could be executed via SQL Injection. The problem was in the expandArguments() method in abstract DatabaseConnection class (\drupal7\includes\database\database.inc) extending PDO. In this method, under the nested foreach loop iterating over the $data array as $i => $value, the $i variable is never sanitized as it is assumed to be a incrementing integer. However, this is not actually the case when posting inputs with the name attribute as arrays, such as:

<input type="text" name="pass[pass1]" value="Password 1"> <input type="text" name="pass[pass2]" value="Password 2">

The above will be posted as:

$_POST = array( ... 'pass' => array( 'pass1' => 'Password 1', 'pass2' => 'Password 2', ), ... ); $_POST = array(    ...    'pass' => array(        'pass1' => 'Password 1',        'pass2' => 'Password 2',     ),    ...

In this case, you will actually be iterating over the pass array with $i being ‘pass1‘ and ‘pass2‘. All other inputs are sanitized, except for these. This is where SQL could have been injected to exploit any website running Drupal.

Drupalgeddon SQL Injection

Drupalgeddon Injected SQL

Drupalgeddon Hacked

This vulnerability could be easily fixed with wrapping the $data array with the array_values() function, although you could have also updated to Drupal 7.32. This function makes any associative array into a regular array with incrementing keys.

From:

foreach ($data as $i => $value) { // This assumes that there are no other placeholders that use the same // name. For example, if the array placeholder is defined as :example // and there is already an :example_2 placeholder, this will generate // a duplicate key. We do not account for that as the calling code // is already broken if that happens. $new_keys[$key . '_' . $i] = $value; } foreach ($data as $i => $value) {  // This assumes that there are no other placeholders that use the same  // name.  For example, if the array placeholder is defined as :example  // and there is already an :example_2 placeholder, this will generate  // a duplicate key.  We do not account for that as the calling code  // is already broken if that happens.  $new_keys[$key . '_' . $i] = $value;

To:

foreach (array_values($data) as $i => $value) { // This assumes that there are no other placeholders that use the same // name. For example, if the array placeholder is defined as :example // and there is already an :example_2 placeholder, this will generate // a duplicate key. We do not account for that as the calling code // is already broken if that happens. $new_keys[$key . '_' . $i] = $value; } foreach (array_values($data) as $i => $value) {  // This assumes that there are no other placeholders that use the same  // name.  For example, if the array placeholder is defined as :example  // and there is already an :example_2 placeholder, this will generate  // a duplicate key.  We do not account for that as the calling code  // is already broken if that happens.  $new_keys[$key . '_' . $i] = $value;

Drupalgeddon was such a major issue because it affected every single version of Drupal 7 before the 7.32 security update addressing the issue. It’s security risk was rated at 25/25 by the Drupal Security Team. It was also such as easy way to inject SQL. In addition, hours after posting the Security Advisory, there was a Public Service Announcement revealing backdoors were implemented, and would remain even after upgrading to 7.32 providing continuous access to your site from hackers.

This post was created with a video for Google Code-In 2014 to explain Drupalgeddon, and why it was such a major issue.

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Dec 22 2014
Dec 22
[embedded content]

Entity Embed is a module created for Drupal 8 by Chandan Singh for his Google Summer of Code 2014 project. It allows you to embed any type of entity such as Content, Comment, and Role. This blog post was made for Google Code-In 2014 to test and review Entity Embed.

Installation

The module was easily cloned into /modules/contrib/ from the GitHub Repository. From there, the module was activated. An error occurred while installing, which resulted in a blank white screen. Although, I was notified by Chandan Singh that this is a known error, and in spite of the error, the module should work fine. After a refresh, the module seemed to be installed, and no error was seen. The install process was easy, however the error might worry a user.

Configuration

Configuring the module was fairly straightforward. An Embed Button needs to be created for each entity type wished to be used in CKEditor. Once the buttons have been created, they can be added to the CKEditor interface in “Text formats and editors”.

entity-embed-ckeditor-config

As it is right now, it is beneficial for those who will use this module to embed only certain entity types since there is one button for a single entity type, however for people who want to embed many entity types it will be difficult to distinguish between the buttons since they all have the same icon. Although an icon/image can be uploaded for the buttons, they might not know how to make the icons.

Usage

Using Entity Embed in CKEditor while creating a new article or page was very simple. After clicking the “E” button, typing in the title of the entity, and configuring, the entity could be seen in the WYSIWYG editor.

Entity Embed Edit Content

One improvement for user interface is to keep the title in the box and load the ID/UUID in the background instead of showing the ID/UUID in the textbox. Also, some “Display as:” values show the same content in the editor. For example, both “Author” and “Label” show the title of the entity. Issue Reported Here. “Display as:” problem relates to another module.

Entity Embed Embedded First Post

Conclusion

Overall, Entity Embed is a great plugin! It can easily be used to embed any type of entity into the WYSIWYG CKEditor in Drupal 8. With some improvements here and there Entity Embed can be perfected!

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Dec 13 2014
Dec 13
[embedded content]

There is an incredibly easy way to create multiple Drupal sites. You don’t need to duplicate the code. You can have multiple sites, with different content, users, and themes since each site can use a different database. They will all run on a single codebase and will be able to share modules. This is one of Drupal’s greatest features called Multi-site. It was first implemented in Drupal 4, and it is here now in Drupal 8.

Getting Prepared

To get started, go ahead and download Drupal.

Then, upload and extract all the files into a new folder for Drupal. For example: /public_html/drupal/

I will be creating two sites, one for dogs, and the other for cats.

These multiple sites can be accessed either from subdomains or subdirectories. If you are going to use subdomains, go ahead and create them. However, if you are going to use subdirectories, hold off until later, since you will have to remove it anyways.

I want the dogs website to be accessed by a subdomain and the cats website by a subdirectory, so I will create dogs.kalose.net

Now create a separate database for each site. Also, create a user or two to access these databases.

I will name my databases user_dogs and user_cats which will be accessed by user user_drupal who will have privileges: ALTER, CREATE, CREATE TEMPORARY TABLES, DELETE, DROP, INDEX, INSERT, SELECT, AND UPDATE.

The Technical Part

Coding

Navigate into the directory: /drupal/sites/

We need to tell Drupal we are going to have multiple sites. To do this, create a file called sites.php. You can find extra examples and documentation of how to set it up in the example.sites.php file.

This file will contain an $sites array containing all the rules. For each index, the key will be the formatted address the website will be accessed at, and the value will be what folder to look in.

Format of each index:

'..' => 'directory'.

My sites.php:

<?php $sites = array( // http://dogs.kalose.net/ 'dogs.kalose.net' => 'dogs', // http://www.kalose.net/cats/ 'www.kalose.net.cats' => 'cats', ); /* $sites = array( // http://www.drupal.org:8080/mysite/test/ '8080.www.drupal.org.mysite.test' => 'example.com', ); */ $sites = array( // http://dogs.kalose.net/ 'dogs.kalose.net' => 'dogs', // http://www.kalose.net/cats/ 'www.kalose.net.cats' => 'cats',$sites = array( // http://www.drupal.org:8080/mysite/test/ '8080.www.drupal.org.mysite.test' => 'example.com',

Since we said dogs.kalose.net was going to use the /dogs/ folder and www.kalose.net/cats/ was going to use the /cats/ folder, these folders need to be created: /drupal/sites/dogs/ and /drupal/sites/cats/

SSH / Shell / Terminal

Connect to your webserver via SSH. If you’re on Windows, you can use PuTTY.

The /drupal/sites/default/ site will have default files we need to copy into each of our sites. Copy these files using the cp command in the sites directory:

cp default/default.settings.php dogs/settings.php cp default/default.settings.php cats/settings.php cp default/default.services.yml dogs/services.yml cp default/default.services.yml cats/services.yml cp default/default.settings.php dogs/settings.phpcp default/default.settings.php cats/settings.phpcp default/default.services.yml dogs/services.ymlcp default/default.services.yml cats/services.yml

Now we have successfully set up Drupal for multi-site. Although, if you visit dogs.kalose.net or www.kalose.net/cats/ it will be blank. Why? How does the web server know to point these sites to the /drupal/ directory? This is why we need to make symbolic links from /dogs/ and /cats/ to /drupal/

If a subdirectory is created when you create a subdomain, delete that directory.

rmdir dogs rmdir dogs

Now time to create the symbolic links. This is why we did’t have to create the /cats/ subdirectory. The symbolic link will automatically point /dogs/ and /cats/ to /drupal/ like they are the same folder.

ln -s drupal dogs ln -s drupal cats ln -s drupal dogsln -s drupal cats

* You may notice that these folders/links have all permissions (chmod 777). You don’t need to worry about this because these permissions won’t be used, instead the permissions of /drupal/ or the file/folder you are linking to will be used.

Finishing Up

All that is left to do now is to install Drupal. I can visit dogs.kalose.net and www.kalose.net/cats/ and set them up using databases user_dogs and user_cats. Using Multi-site, these sites can be completely different except for the fact that they share the same underlying code.

Share this:

Author: Akshay Kalose

A teenager, who is interested in Computer Science, Information Technology, Programming, Web Designing, Engineering and Physical Sciences.

Jun 04 2014
Jun 04

Currently Drupal core does not offer any hook to do actions after a node/entity is inserted/updated/deleted in Database. So for example you can not send an email mentioning the node after the node is inserted because Drupal uses SQL transactions and the node is not yet fully written to database when hook node presave is called so if for any reason the transaction is rolled back, users will receive a false mail.

So Hook Post Action module introduces several new Drupal hooks to overcome this limitation
  - hook_entity_postsave
  - hook_entity_postinsert
  - hook_entity_postupdate
  - hook_entity_postdelete
  - hook_node_postsave
  - hook_node_postinsert
  - hook_node_postupdate
  - hook_node_postdelete

Apr 30 2014
Apr 30

This gonna be short and sweet.

If you need/want the Drupal Update Manager to work through SSH then you need to install the “libssh2-php” php package on your Ubuntu server. You know the Update Manager; it’s the admin interface when you install a module or theme, or more importantly if you are doing system-wide updates.
Update Manager

If you do not have the “libssh2-php” package installed then the only option you will have is FTP.
FTP Only

Unless you have a very specific reason, you do not want to run an FTP server on your Ubuntu server. Especially, when you have alternatives like SFTP and SCP for transferring files and they are based on SSH.

Now to enable the SSH option on the Update Manager page, you need to install the “libssh2-php” package and reload your apache server.

apt-get install libssh2-php
service apache2 reload

Now you have the SSH option on the same page.
SSH Option

Well, that being said, using Drush would be a better choice for these operations but there might be times where you need this.

Share this:

Like this:

Like Loading...

Apr 28 2014
Apr 28

When I got my first VPS (from Linode) like 4 years ago, for heavy Drupal use, I read a lot of guides about setting up a LAMP stack on Ubuntu. At first most of those guides I read and followed were Drupal specific but later on I read a lot of non-drupal, LAMP stack related stuff as well.

In addition to the guides I read (and still reading), now I have 4 years of experience and knowledge that I learned by trial & error. Not to mention that I have a long System Admin (Windows only) and Database Admin (mostly Oracle) past. I still wouldn’t call myself a full-blown Linux System Admin but I believe I have come quite a long way since then.

Now I am thinking about the guides and wondering why none of the ones I read does not tell people to delete the default site configuration that comes enabled upon Apache installation. As if this is not enough, almost all of them relies on making changes on that default site config (Drupal or not).

99 times out of 100, you do not want/need a default site running on your server; which will service to any request that finds your server via IP or DNS; unless the request belongs to a website that you specifically configured. And I am sure you don’t want your apache to service a request, let’s say, http://imap.example.com unless you specifically configured a site for imap.example.com.

One of the first things I do is to delete that default website.
I can either delete the symlink…

cd /etc/apache2/sites-enabled/
rm 000-default.conf
service apache2 reload

or you can do it by disabling the site with “a2dissite” command. Some might say that this is the proper way to do it but actually they do the same thing; removes the symlink.

a2dissite 000-default.conf
service apache2 reload

As you have noticed that I did not actually delete the default site configuration file which resides in “/etc/apache2/sites-available/” I have only disabled that site. Who knows, I might need that file in the future (for reference purposes most likely).

Now the question pops in mind; the guides you follow tells you to make a change in that default site config file. Of course the changes will not have any effect since the default site is disabled. As for Drupal, it will ask you to change “AllowOverride None” to “AllowOverride All” in the below shown block.

        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted

This is how you do it. Open your “apache2.conf” file, where your real defaults are set. Find the same block and make the same change there.

cd /etc/apache2/
vi apache2.conf
##  Make the changes  ##
service apache2 reload

This is on Ubuntu 14.04 …

Share this:

Like this:

Like Loading...

Apr 27 2014
Apr 27

If you have upgraded (or planning an upgrade) your Drupal 7 platform to Ubuntu 14.04 then you most likely know about the “install creates 31 tables then stops” and “Installation failure when opcode cache is enabled” issues. Which is caused by a problem between the Drupal code and OPcache.

A few words about the OPcache. Ubuntu 14.04 comes with php 5.5, which has Zend OPcache built-in. If you have already tried to install APC extension for your php setup, you failed. And if you googled about this failure then you heard that the APC is included in php5.5. Well, you can say that. Actually, the type of these caching solutions are called “OpCode Cache“. “APC” is one of them. “Zend OPcache” is another one; and this Zend OPcache (or OPcache for short) is built into php 5.5, not APC.

The Drupal problem has been fixed for D8 on this issue but no patch is available for D7 yet.

The workaround is to disable the OPcache, which is enabled by default. It is a setting in php.ini file.

opcache.enable=0

The question has been raised if disabling the OPcache before installation and enabling it right after would be good enough. While I don’t have a solid answer for that, it should be good enough to keep it disabled during installation and upgrades. I permanately turned it off on my test site. Maybe I should turn it on again and do some tests..

Another question I have seen but not answered was, if we can disable the OPcache per site basis. Like disabling it for a D7 sites and enabling it for others.

Yes, we can do that. As the title of this article suggests, we can disable OPcache per site basis but we cannot enable it whenever we want it; it should be enabled by default. If you have disabled it through php.ini file, then you need to revert it back.

Placing below line in your “settings.php” file will disable it.

ini_set('opcache.enable', '0');

However, I like the “.htaccess” method much better.

php_flag opcache.enable Off

Remember that your apache config should have “AllowOverride All” in order to make the .htaccess method work; which is also a requirement for installing & running Drupal websites.

Share this:

Like this:

Like Loading...

Jul 09 2013
Jul 09

If views of my presentations on SlideShare are any indication, a whole lot of you are interested in integrating Drupal and Alfresco. Despite the fact that the presentation is four years old, it consistently makes the “most viewed” list out of my uploads. If you are considering Drupal but need something a bit more document-centric to serve up your files as part of that Drupal site, take a look:

With over 12,000 views, it is safe to say there is definitely something to the combination of Alfresco and Drupal.

Another apparent classic is:

Which is kind of scary given its age and brevity. I think the popularity of this is due to the seemingly inexhaustible demand for “getting started” resources for new Alfresco developers.

This one has similar info, but with more details, and is probably a better choice for developers trying to get an extremely high-level overview:

The CMIS API is now the preferred way to interact with the Alfresco repository remotely, and many people use this presentation to get a quick overview:

In fact, I’ll have a CMIS powerhouse panel on Tech Talk Live tomorrow (July 10, 2013). So if you are just getting started with CMIS, please join us.

If you like CMIS but you don’t want to fool around with your own server, you can use Alfresco in the Cloud. This deck gives a CMIS overview and discusses the Alfresco API at a high-level with links to sample code and screencasts:

Thanks to everyone who has made use of these presentations!

Jan 04 2013
Jan 04

I'm writing this at 4am after my 2-year old woke me up because he was cold and hasn't figured out how to put a blanket on himself yet. Surely, if he knew the kinds of pains I go through every day to plan the future and make a living without losing my sanity, he'd learn how to put on his own blanket. This is also after waking up at 3am when my 4-year old climbed into bed between my wife and I. There was more room in his bed, so from 3-4 that's where I slept.

It's my fault, I suppose, for deciding to have kids - something I'd been planning for long before I had any. I started teaching myself programming and graphic design 12 years ago because I wanted a viable skill set that would allow me to support a family and spend a significant amount of time with that family. After numerous career steps, I eventually founded BuildAModule, in a large part as a way to leverage work-related time more carefully. And you'd think that by now I'd have balance, but even after over a decade of practice I struggle every day with juggling work time and family life with everything else that comes packaged with typical human existence.

So I write this as a way to connect with any of you out there who also feel like you're trying to figure out how to straddle multiple worlds without ripping your pants. There's a few things that have helped me, and I'm hoping to hear some voices chime in on this as well. Some of this stuff you've probably heard before, but all of this I've tried putting into practice, and this is a summary of my experiences. This is also partially a response to a question about post-child life in a Drupal world.

There is always something amazing happening you will never know about

I've started to realize that no matter how much time I spent learning, consuming and observing, there will always be more awesome things happening than I can possibly ever even hear about, much less dive into. Even in the microcosm of Drupal, I'll never have enough time to learn it all, or keep abreast of everything that's happening.

The cool thing about this is that it means that everyone else is in the same boat. Chances are that if I choose a small niche to develop skills in or stay current on, that I'll know something or contribute something that will help other people, and that's really what it's all about, right? We learn in order to help others in some way, sometimes as part of our living, sometimes as our life's work.

Even when free time is cut in half or quarters by the addition of children into the picture, I've found that the same rules apply. Freaking out about how much I don't know just saps the energy I need to do something worthwhile with the time I do have. And when I can find joy in what sometimes feels like the most minor contributions I make in this world, I tend to want to make even more.

There is a difference between the amount of time you want to spend with your kids, and the amount of time they need

As I was prepping for family life - building skills, saving money, trying not to annoy my wife too much - I wanted to make sure that eventually I could be available to my kids as much as I wanted to be, and as much as they needed. Now that I have kids, I realize that there are actually two separate things. How much time I want to spend with my kids is not necessarily the same as what they need.
Before I had kids I thought that I would be happy spending all day, every day with them. Now that I have them, I think the idea of staying sane in that scenario is a little wonky. The stay-at-home parents I know all feel a little nuts, and judging from time I've spent with my kids solo for long periods of time, I completely understand. They're awesome in every way except that they have a completely different agenda than I do and operate at a very different pace than adults. It's endearing at first, it even helps me see things in a fresh, youthful way, but after a while it becomes impossible to feel like a normal person around it. Even though part of me really wants to want to spend all my time with my kids, there's some other part of me that really needs outside input, and if I don't get that input, I'm not going to be very awesome with my kids.. And I've noticed my kids are the same way. Without more diverse input from other people, I drive them crazy, too.

If my goal as a parent is to make sure that my kids develop normally (even amazingly) and don't experience unnecessary trauma, then they actually need to spend a good bit time with other adults and kids, because what I can offer them is relatively limited when compared to what a community can offer. As a parent, though, I can offer consistency and stability from day to day. I can be a fixture, whereas the community is fluid, and I don't think it takes that much time to function as that fixture when you're present nearly every day.

The ideal time ratio between work and family

In our current balance of work and family life, I spend 3-4 hours with the family in the evening, and then I'm around most of the weekend, with maybe a 4-hour break during the weekend to do some introspection or play some games. Every 2-3 months I end up taking a week or two off, and during that time I spend the bulk of my time with the family. Even though I'm grateful for this time and I feel like I get the opportunity to spend more of my time with my family than a lot of people do, it often feels like it's not enough.

Something interesting happens when I'm around the family more. Things that are normally stresses - kids having tantrums, silly bickering between the adults - tend to fade away. We all still have moments, but the extra time also seems to build a buffer of understanding and compassion. I tend to be happier, and so does everyone else. It's that effect that makes me wonder what would happen if I didn't work as much on a regular basis. It makes me think about how different work ethics are in other places around the world, and how maybe those families might potentially be a lot happier on the whole.

Even within this country, I get the feeling that each person's comfort level around the work and family life ratio is enforced by their individual communities as well. If you hang out with people who work a lot, you'll probably be more comfortable working more, and more thankful for small breaks to be with family. If you're surrounded by stay-at-home parents, you'll probably feel like work is keeping you from immersing yourself in family life. I have a little bit of both, since I work at home and also hang out with some pretty hard workers. So, I go back and forth between feeling bad about not spending enough time with the kids, and not spending enough time with work. Sometimes I'm right in the middle, which is what I'm attempting to cultivate more of.

Employeeship, freelancing, and product-based businesses

I pursued a freelancing career a few years after graduating from college so that I could work from home and maximize income. Ideally, I wanted to make enough to work part-time when I had kids. I got a job in a campground that afforded me the time to learn what I needed, and the isolation to keep distractions away. I think I got pretty lucky there.

A couple years ago I shifted my focus to work on a product-based business, which - I learned from The 4-Hour Workweek - was maybe the only way to break through an income ceiling that comes from working hourly. Since making the shift, I significantly increased my income, and have been able to take large chunks of time off with family, which is exactly what I was hoping to do from the start.

For those who crave more time with their families and are currently employees, I have a hard time thinking of another way to move forward besides starting to work for yourself. Freelancing comes with less risk than attempting a product-based business right off the bat, and gives you enough wiggle room to start building a self-funded product while also giving you the same wiggle room for family. Lowering your financial requirements can also add flexibility. I've given a couple talks on this subject, and if you're thinking about making some steps forward, definitely post something here so I can add a few words of encouragement.

That said, starting a business can also be a bigger time-drain than being an employee, depending on how you approach it. I gave myself a 6-month window of time to get to a certain level of stability with my business before going back to freelancing. If it hadn't have worked out, I would have needed to give myself some time to re-connect with the family.

Talk to yourself

It's hard to find the right people to talk to at just the right times when if you're trying to figure out how to slice the work and home life pie. And usually I don't even know what questions to ask. Luckily, there are two of me. Or at least, there's two parts of my brain that can talk to one another to some degree. So, when I'm feeling a bit lost, I'll start writing (there's a nice, distraction-free program called Writeroom that I use), and usually after 30 minutes or so, I begin making progress. I start being able to articulate the problems and think about some possible re-orientations or re-organization that I could try. 

There's been plenty of research into how words affect our thoughts, and I've noticed that whenever I start using language more in my life - either talking to other people, writing blog posts or journaling - that my thoughts also become clearer. I'm not sure if this works as well for other people, but it's definitely a key technique in my arsenal, and what I use whenever I'm trying to re-balance family and work life.

Meditation on the meaning of work

It's been easy in the past for me to take the idea of work for granted. If we want to sustain a certain standard of living, we need money and thus have to work. But a few other things come into play that affect how much we decide to work. 

Through work we can potentially impact lots of people and affect world around us, and that can be a very powerful draw, one that sometimes conflicts with being present as a parent (or any other role, for that matter).

For me, it's helped to think about what my life would look like if I no longer had to worry about money. This allows me to recognize the distinction between the desire for financial stability and the desire for other benefits that work brings, like challenge and positive impact on other people.

Because financial troubles tend to cause a huge amount of stress in families, I think it makes sense to trade the time I could be spending with my kids to achieve the goal of financial stability. But when I have that foundation set, it gets a little more difficult to draw the line between work and family life when it comes to the other benefits associated with working hard. 

At what point do the more meaningful aspects of work become less or more important than spending time with my family? That answer changes all the time for me, but I know that I only have a few more years before my children start to have richer lives in the community with school and activities, which means less time at home, so I feel like there's something special in the next few years that I don't want to miss out on. Most of us only get to be parents once, right?

I try to take time regularly to think about the benefits of pursuing work versus spending more time with family. It's easy to fall into a pattern of work and family life and forget why I established the pattern in the first place, and sometimes when I'm working through the thought process around it, I remember something important that completely changes what I do or why I'm doing it.

Put the oxygen mask on yourself first

Most parents will suggest that you have to take care of yourself before fretting over your kids (beyond their basic needs, of course), because if you become compromised in some way by forgetting yourself, your kids will suffer. When I find myself going nutso over kid meals and activities and cleaning, and I re-orient myself to think this way, it can make a big positive impact. Usually when I'm on the more happy side of the emotional meter I end up being a much better parent (or at least feeling like I am). Also, kids are pretty dang resourceful and all of their mechanics are bent towards self-preservation. So even if you forget to feed them because you're reading a book or building a lego sculpture, they'll give you a heads up before they get even close to passing out.

To me, this means that if I'm getting a lot of satisfaction from work, and it's important for me to focus on it to keep a certain amount of mental and emotional momentum - or that I'll feel some resentment by having to give it up - then my kids are going to be fine if I keep that focus. But as I mentioned before, the more time I spend with them, the better we get along and understand each other, so when things start getting a little too stressful in our relationships, I'll put aside work for a while until the family is running a bit more smoothly.

Kids brains know what they need to learn, so relax

This took me a while to figure out, but it's taken a lot of stress out of parenting and also frees me up to optimize the time I do spend with the kids. Research shows that kids know when a part of their brain is ready to develop, and all we have to do is take queues from them and do what we can to support it. This is much, much easier than trying to get up to speed on everything kids are meant to be learning at different stages, and trying to force it. There are still lots of things that I've initiated, like reading aloud and singing songs, but I don't feel like I'm a complete failure if they don't pick up on it.

Any random kid is funnier than virtually any adult

I've laughed more since having kids than I did for the previous 15 years. I've noticed that when I view my child as a comedic relief, rather than a responsibility, I can derive a lot more enjoyment from them. This perspective doesn't work all the time, but sometimes it's perfect, and with me finding it funny when they spill the milk for the 5th time today, they end up having a lot better time with their own foibles, too, making us have a much better time with each other.

Kids also make excellent fodder for humor, and I actually think about certain comedians a lot when I'm with my kids, it helps me not take it too seriously. Louis CK and Bill Cosby are the two I find the most helpful.

The role of exercise

Exercise takes time. But for many of us it also will extend our lifespan. So, save the time now and potentially kick the bucket earlier? Or hit the gym every day for an hour or two and miss out on making progress on some front, whether it's family or work?

Part of the answer might be had with a treadmill desk. I've been using one for 4 years when I work. It can be cheap and doesn't negatively impact focus. I've turned on a  few people on to it over the years, and most have lost a significant amount of weight as a side effect and feel a lot better since using it. I find it helps me focus, much like a stroll outside would. This isn't really the cardiovascular workout you'd need to meet the requirements for exercise by certain measurements, but it's way better than nothing, and it feels pretty awesome.

I also go in and out of phases of exercising or going the route of using that time for work or family. But I do find that when I'm exercising, I have a noticeable increase in the amount of energy after work for playing with the kids and being more interactive with them. I'll also go back and forth on if I take this time from my work day or if I take it from family time.

Avoiding burnout by finding focus

The demands of a household are infinite. You really can occupy a team with everything that could be done in a home. And working in Drupal feels the same way. There's always something new to learn, a potential client to pursue, an awesome idea to make a reality. There are about 400 items on my list of things that I'd like to do if I had the time. Maybe half are Drupal-related, and the other half has to do with family and other skills I'd like to develop, or things I'd like to learn. I have to go back and forth between Drupal and family and focusing on other aspects of personal development because if I didn't, I'd start to resent whatever was keeping me from everything else. In order to stay active at all with Drupal, I have to find a balance, and I do that by focusing. 

I really want to release a new Evernote module, I desperately want to climb the core contrib ladder, I crave participating in IRC and I want to hit up twice the camps I do. But, I'm focusing now on building a really good learning resource for Drupal, and that might be all I'm able to get to for a little while, because I also really want to be around when my kids do amazing things.

I think it's important to want to do more than you can do, but also to somehow be okay with your own limitations. I focus on a couple things at a time, and I've found that pattern works the best for me.

Sep 25 2012
Sep 25

I've known that TomRandall and the rest of the crew at Level 10 have been working on Open Enterprise for a while, and I'm excited to have a reason to give it a go. I'm in the process of numerous upgrades to BuildAModule, one of which is making the current bare-bones blog more integrated with the technical advances of the last decade.

Since it's been a while since I've spun up a Drupal-based blog, and since I know there's numerous aspects of blogging and content creation I'd like to finally wrap my mind around and implement (like RDF, HTML5, pingbacksSEO Tools), I'm thinking that the right distribution could save me time. I'm not entirely sure Open Enterprise has everything I'd like to integrate, but I know it has some good minds behind it. Or, it could turn out that I'm not the target use case for Open Enterprise and I could spin my wheels a bit, but I'm going to take the gamble so I can get familiar with a product that I know is on the edge of some awesomeness. 

So, here's an 'unboxing' of Open Enterprise. In particular, I'm using the Enterprise Blog version, which I'm thinking might be the same as Open Enterprise at it's core?

As per a typical Drupal installation, I downloaded the source, set up an empty database, added a virtual host via my MAMP Pro, and went to the new site.

The installation process gave me an Open Enterprise installation profile, and prompted me to install a set of 'apps'. I was tempted to check everything:

After all that installed, it had me fill in the default admin user and site information screen. After I submitted that, I hit a screen asking me for my FTP information. That threw me off a bit:

I tried to submit the form with nothing in it, then filled it with bogus information, thinking that maybe I could deal with it later. But neither of them let me pass. I did a quick search in the issue queue, and pulled this up which said that I could just make the sites directory writable, which I did by cd-ing into the Drupal directory via Terminal and doing a

chmod -R 777 sites

I remember running into something similar on Pantheon, I imagine for similar security reasons. But I'd sure like to have had a few more details on the screen about why this was needed.

After I did the chmod, I went ahead and refreshed the page, which seemed to push the installation process to the next step. However, not without a few errors (a lot of which just have to do with my level of reporting):

Some of these errors had to do with images, and as I started clicking on bits to look at content, I saw that there were no images and I was getting errors like:

Messing about with SEO tools

So, the first thing that I want to play with is the SEO tools to see if there was anything in there that would clearly be a benefit to my current workflow, so I go to create a piece of content and just copy over a blog entry from my personal blog.

Next, I scrolled through the vertical tabs at the bottom of the content and clicked on the Content analysis one. Everything was enabled except for the Alchemy item, which got me curious, so I followed the steps to set it up, which required the following:

  1. Downloading the PHP SDK files from here.
  2. Putting the files in the AlchemyAPI folder in the alchemy module folder (but you have to take the files out of the expanded folder)
  3. Going back to the content and trying again. This time I need an API key, so I follow the instructions
  4. I sign up for an Alchemy account, verify my email, request the API key, copy it and paste it in

Another disabled tool was the Readability section, and I didn't want to dig into that after going through the 10-minute Alchemy setup. But I did watch some of Tom's video on it  which was good (Tom has a nice screencasting voice).

Once Alchemy was set up, the check seemed to go through and I got some additional info:

After checking out the 3 tabs there, nothing appeared to be particularly useful for this article at least.

The Quick SEO tab had some good tips, mostly I think for folks just getting a feel for the amount of content to plug into a typical article:

At this point I think that maybe I should save the article, since it's showing as just having 12 words when there's definitely more, and realize that I pasted the content into a WYSIWYG. My content contains html tags, so I want to paste source in, and I don't see any source code icon (that would have been nice to have).

So I went ahead and copied my content straight from the public facing page into the WYSIWYG, which got everything including some object tags I had in there, and after that there were some better results from Alchemy:

I played around with the SEO tools for a bit, and couldn't get the keyword search to work. I noticed that you could hover over keywords like you see above to show menus for adding keywords to some kind of list, but after adding a few items I wasn't able to find where the list was. I imagine there's a bit of a learning curve here, but it seems like there's got to be some potentially good stuff in these tools.

Next, I wanted to see what the output of a typical blog post looked like, so I checked out the source code. Woah! I haven't seen that many CSS and JS files on a page before. But, I know you can turn on aggregation, so I ceased the freak out and scrolled down to where the body started:

I've been diving deeper into HTML5 over the last several weeks, and I was hoping to see some

and stuff with oddly named attributes, and I wasn't disappointed (though I'm curious why the footer was at the head of the article). I'm still wrapping my mind around best practices around HTML5, and I imagine that looking at this might contain some good lessons. 

In the Open Enterprise description it mentioned that it's using a flavor of Omega for a responsive theme, so I played with the browser window size a bit to see what changed, and got a re-sized logo and stacking in the navigation on the smaller window size. At an even smaller window, the sidebar drops down below the content. Cool.

When I go to check out the blog page as an anonymous user (after publishing the page), I get Access Denied. Bummer! ;)

Okay, so permissions are set up to keep anonymous users at bay. NO BLOG FOR YOU! Well, I kind of want people to read this stuff, so I went to the permissions page and made a couple minor adjustments:

And

Hey, that blog page looks pretty good! Except for a little floating issue with the comment buttons:

So at this point, I want to see how hard it would be to integrate Disqus with this blog. I first go to the modules listing page on the off-chance that it's already included. It's not, and I noticed while I was there that Drupal core and the theme is already a bit outdated:

It's tough to keep up with Drupal, man. 

I knew from talking with the Level 10 folks that they were really getting into integrating Apps, so I wanted to check out the App listing to see if I might have missed anything. In the admin bar I clicked Apps and then took a look. I didn't see anything there, so now it's time to download a module and see how well it integrates with the distro.

Okay, that took a few minutes but everything went smoothly. I now have Disqus enabled on blog posts, and normal commenting disabled.

Now I want to click around a bit. I click on the Images tab, and WHAT'S THIS? Whirlyball? And who's the psycho int he middle who clearly runs the operation?

This default placeholder content is way better than what my real content is going to be. ;)

When I click on HOME, I get a Page not found:



Bummer again! The Add URL redirect link is enticing, but when I click that it looks like it would set the redirection for any 404s, rather than just the home page:

Okay, so I'll leave that as is for now.

As my next task, I want to set up those social links at the bottom of the page to point to something functional. Icons would be nice, too, but I'm suspecting this is a standard Drupal menu and getting those icons might take a little pulling of teeth:

Indeed, there's a little cog wheel that's a bit hard to see against the blue, that points me to edit the Social menu, where I update my links.

Getting my share on

Now, I want people to be able to share posts, ala ShareThis. So, I take a gander through The Configuration menu and see a Social media item, which I click on. As I scroll down, it looks promising:

Okay, but how do I get these to show on my blog posts? As I'm looking around, I run into this, which looks a little off:

I'm also getting some overlay screens where there's no way exit because (I'm guessing) the close button is under the admin toolbar:

Then I browse to the modules page and see that maybe the social module I'm seeing isn't fully functional yet:

I check out the Help page and it mentions something about one's profile, so I think that maybe I have to associate the social media accounts with my user, rather than the site, but when I go to my user account page, I just get a submit button:

So I scroll down the modules listings page, just to see what's in there. And what's this?

This looks promising. So here's what I do:

  1. Enable the Widgets module
  2. Check my blog post to see if social icons magically appeared. They didn't.
  3. Went to the Widgets module page to see if there was any insight there. Indeed there was!
  4. Based on what I read, I went to the blocks configuration page and enabled the Widgets: socialmedia_share-default block in the Content region, above the Main page content block.
  5. I went back to the blog past, and whammo!

Okay, so I kind of like the ShareThis versions that include details about the different networks' activity (though I just found out you can up the count just by clicking on the icons), and based on the info on the Widgets module page, I probably just need to include the Service Links module for that.

Things to investigate and update

So I'm liking this so far. I have a responsive theme that doesn't look like crap, I have a lot of my needs anticipated in terms of blogging setup. I'm on Drupal 7, which feels like the future after working on Drupal 6 so much these last 6 months, and it seems like the feature modules / apps are thoughtful and goal-oriented, meaning I can probably get some nice functionality pretty cheap if I have the need for additional features later on.

At this point, there's a few things I want to check out / fix / accomplish / learn:

  1. Get the site branded.
  2. Figure out how to interlink pages with automatic Related to this article functionality for better SEO.
  3. Set up archives and a tag listing blocks or pages for better SEO (correct me if that's old school thinking).
  4. Check out how forms do on the responsive front.
  5. See if enabling the view source on the Wysiwyg is going to break a feature module
  6. Research current best practices on working with Features in a distro (I'm curious if anything has changed since I put together a video series on the subject)
  7. See if I can use Alchemy to automatically tag and enrich posts for search engines, ala the AlchemySEO service / product. And also, is that kosher?
  8. See why I can't get keyword searches to work. Is there some API key I need?
  9. Check out RSS feeds. And is there any integration with PubSubHubUb, and do I really need to care about that?
  10. What about trackbacks and pingbacks? Do people use those? Any reason they weren't included in the distro?
  11. Play with OpenGraph MetaTags to see how it impacts mentions in Facebook
  12. I'm curious what the UUID module means for nodes. Can I use them in feature modules now (the last time I tested that it was buggy)? Is that what it's intended use is in Open Enterprise?
  13. Integrate a menu-search tool like Coffee, or maybe finally check out the Drupal 7 port that Amit Goyal made of Navigate.
  14. Do some Adobe Shadow (or Edge Inspect as it was newly released today) to check out how this Omega theme looks on a few different devices. And make sure images are 100% max-width (they get cut off as the screen gets smaller).

Thanks!

Thanks to the Level 10 folks for putting this distribution together. I know it's super hard to build features that work for lots of use cases, but it seems like they're doing a really good job so far. I'm looking forward to seeing how fast I can get this to production. :)

Apps Images enterprise I ma e 5. Is ani app for m ? h'1 Images content and displaying image ApPS are the next generation In usability for Drupal They contain bund les of galleries tuncncnatnv for your bS'lte. Select any apps you want to install Fig hr new You a add rmln? jailer rim line I'll pm page i Link Mil.11.l1gr!'; rmf ns- &quot;,uluru' link. It can be used to keep short I ? gf valuable resources In odder lay Inman apps. you Muni be able Il I FTP er SSH in! C tour server This uses v. r, nr the basis for fiJi blqw;!1 link directory like dmo.T.org. same [process as the update module. locations rr-r APPS TO ? ALL Enables r&quot;r'hl:rl.l4.e me n L od locations as content address are automatically go eneere map displays you Cari 'i?iY groups or locations on Of as listing april rotating balder j blog rotating banner of sudes with links Vern ()ll·illmC'1 on the homE page multiuser full featured avg app. Has many at the features .M' SEa essentials Development essential &quot;1';trrh engine cptlmI7.,tifl a features for your sea is il, more this app ands the most commonly used devel omern medulas to your site if yogi are limited set of than thaw In CFO ToXIcs. If you have Into ? SRN doing development In D r pal I this app its essential Ned robed CIO events a seo Tools provides t(lil1? for managing time bated events includes r h ? Liam Suite of advanced Seo tools including keyword reseercn and content analysis Seo Tools of includes all of the features of co essentials plus many ether tools for going Seo In fan drupal lode to bug In apps you must install 90olille_anaMlcs I'll a n a to. fore this enables creation of answers oLD frequently asked questions using a standard Iraq form, app will install o Forum DNAa! May, community discussion boards with topics and threeued comments help GNU socialize tour Swe. images I'd videos enterprise images is il 11 App for noIr aging images cnntr-n and displaying image provides video content management [naMes playing 0.11 uploaded Dr timbered videos galleries l' them Y,iliU T,u Un ? di WebfOrm Manages online resource links it ean be used tO keep short list or Ell valuable resource eneerea you to add web terms to your Site that can be lemalled and/or saved to a uenaase includes contact us example form but allows you to build any number of cUStom forms DiE:FAULT content ? In Stal default content B, ?r?jr.tli ng this box default confirm will me ,imt;tilDll;ld fgr each app without default content the site may look empty before yOU start addl fl to It. YOU can remove the .iI'efw! l content later by going to line apps caring page install apps SkIp this step Home Update downloaded successfully. WARNING You are not uSing an encrypted connection wyour password Will rnccse profile be sent in plain text, team mere cnccse language TO continue provide your server connection details Verify requirements Connection method se up database 'FTP install profile FTP connection settings Configure site username install apps Finished Password your password not sated In the database and only used to establish cc-reecucn ADVANCED SETTINGS Continue Alchemy has been installed Content Analysis has been installed Content Optimizer has been installed Keyword research has been installed Kilvword research google has been installed wcrestream has been Installed AppS enabled successfully Imponed node 43. Contact uS of i nodes oJt imported Some values may have been reset depending on Node experts configuration Choose profile Choose language Verify requirements Set up database rnstan profile Notes. Undefined vanabte replacements in quid token (line ? 9Sof of jChrlsjWebsilesjloc.b1og bullddmodule.comfslteS/dlljmodules/uuld/uulo taken NotiCe Undefined variable replacements in uuid_tokensO (line 9501 of jChf;sjWebsit?sjlo' blog. bui!d.Jmodul?.com/ sites J/f/rmxlule l/uuid lIuid.1 ket Notice Undefined variable replacements in lJuid_tok.?n'JO (line ? 95 of of IChf;'JIWf'bJit?J/loc blog. build.modulO com/silf'S .t!f/modulu/ uuid/uuld take i NotIce. undefined variable replacements in uujd_tok?nfO (line 95 of /ChrISjWt'!bsilesjloc.b1og bul/d.dmodule.comfsltesfd//fmodult'!sfuu/d, Eula takei Notice Undefined vartabte replacements in uuid rokens(J {line 95 of /Chr;s/Wt'bsites/lrx.bJog,bui/ddmtJdu?.comlsitt'sldll/mtJdu!t's/uu;d/UUid,lClk Notice undefined variable replacements in uujd token (line 95 of IChf;sIWcobsit?'J/loc blog hutld..Jnmdulr com/silf's I .tll/modulu/ uuid/uuld take i notice undefined vanable o. replacements In quid token [line as of ()f /Oms/Web5ltt's/foc.blog,bu,!ddmcdu!e.com/Srles/.l/1lmcdlr!es/uwd/uwd,t(lAw Notice Undefined variable replacements in uuid_tokens(Hline 9501 of IrIdic Ill tgif hills Jri&quot;, in, ,J[ configure sue rnstan Apps Finished Notice: getimagesizeO [function.getimagesize): Read error! in image_gd_get_infoO (line 349 of IChrisIWebsitesl/oc.blog.buildttmodule.comlmoduleslsystemlimage.gd.inc). least 2 to times is recommended Consider mcreasm Content Analysis Results Analyzers Alchemy Alchemy Keywords Concepts Entities Quick SED Term Relevance Keyword research voice testimonial service 95.7% Analyzer checklist Content Analysis Results Close Window Quick SEa Analyzers ep ick Quick SEe rd: testimonial long analysis Analyzer checklist yzer page tnte Char count 95, Word count ? 14, Keyword count ? I. Keyword density 7.1, Keyword prominence ? 28.6 Your page title contains 95 characters No more than so characters to 75 characters is recommended Consider reducing the length of your page title sou Char (ounl.76, Word count. 12. Keyword ccunt-, I. Keyword density_S.l, Keyword prominence. 16.7 You body contains 12 words At least zoo to 800 words is recommended Consider Increasing the number of words. The targed keyword 'tes1imoniar occurs in the body I time At least lto times is recommended Consider Increasing the number of keyword ccecrenees in your body copy Meta keywords Char count o. Word count o. Keyword count o. Keyword density.O.O, Keyv,.(Hd promlnence-O.O You meta keywords contains o words. At least to SOwards IS recommended Consider Increasing the number of words. Content Analysis Results Analyzers Alchemy Alchemy Keywords ? Con Entities Concepts Quick SEO Term aerevaeee Keyword research can widget 99.3% Analyzer checklist Google Voice 98.90 wtb page 83.00 voice testimonial service 63.4% Google Voice page 57.1% Click Call Widgets 55.5% new call wjdge? 53. lg new greeting 44. embed code 35.1&quot; OPEN ENTERPRISE OPEN ENTERPRISE HOME Notice: Undefined variable replacements In BWIA rokensQ fine 95 of /C'm.sIWebsltesJ? tllOg.ooA'damodlJ.le.comlsimslsNimodul'esluuidIUUJd. rokens. Nance: Undefined variable: replacements in ulna tokens (line 95 of of Johns ? ? byog OlJi'd.lrn odlJ ? comlsitrJsfaY/ft'KXk.sPos/uuidfuukJ. tokens l. ACCESS DENIED You are not aumcruee to access this pa ge USER LOGIN E-mail p as s word Create now account Request now password LOGIN CA ? main content ? ay &lt;hI ClASS M title idea pa9c-title&quot; show to quickly set up an Qwcso:r &lt;dlv class: class-&quot;tabS elearfix-&gt;&lt;h2 ? class ? element-invisible&quot; Primary classeN active?&gt;&lt;a ? href ? M /blog/chris-shattuck/how-quickly-set-awesome-customized-f class-&quot; element-invisible-&gt;{active tll:b hrefa·/node/6S/edit? ? href·-/node/6S/kwresearch&quot; ? iliaci idea hret-&quot;/n In class-&quot; action-links&quot; ? hret-M/node/add/enterprise-b1og&quot;&gt;Add blog post! all &lt;11&gt;&lt;a href-&quot; /admin/contont/node/enterprise blog Administer blog ? blog &lt;luI&gt; &lt;div id-&quot;b1ock-system-main&quot; c j as a=vb block block-system&quot; c Laa s=vb block bloc title. id-&quot;block-system-main&quot;&gt; &lt;div class=&quot;content&quot; class: content clear fix &lt;article about=&quot;/blog/chris-shattuck/how-quickly-set-awesome-customized-free­ class=&quot;node node-enterprise-blog node-promoted node-published node-not-sticky sel enterprise-blo9-65 -s- &lt;footer class submitted &gt;&lt;span property: dc: date dc:cre4ted&quot; content ? 2012-0 21:36&lt;/span&gt; &lt;span ? rel=&quot;sioc:has creator pga href=&quot;/users/chris-shattuck&quot; tit! In ? t r. h r; a a hnt&quot;. eueje&quot; T. vn&quot;,n ? RI II a p. r ? n t. nrnnArt-.v= fnltf!nllmlR&quot;&gt;r..hri tsi MW modulI! module D Modules There are updates available for your version of DrupaL To ensure the proper functioning of. updates page for more information and to install your missing updates, There are security updates available for one or more of your modules or themes. To ensure the available updates page for. more Information and to install yOur missing updates. Download additional contributed mOdU'o extend Orupal's functionality. mod Regularly review and install available updates to maintain secure and current site. Always REGISTER for to post comments LOG IN Access the content overview page VIew published content View own 0i;own unpublished content ?content revisions View content IJ.'JCD Administer comments and comment settings View comments Post comments by i oj NRA PAGE NOT FOUND Add URL redirect from this page to another location URL redirects FROM http:!{loc.blog.bulldamodule.com/ navigatlon4Q4 TO. TO Advanced options Add URL redirect fro The requested page could not be Ic Unk8dln Twitter YouTube Submission form settings Title Publishing options Published Promoted to front page Display settings display author and date information Comment settings closed Threading SO comments per page Content Analysis Settings Menu settings Ml sneman Indusio excluded Scheduler settings PubU,hlng enabled Iran DEFAULT ICON style i 32)(32 ill rcee set Styles teverren 16x16 Glossy 32x32 by teverren 48x48 ? in t ? L, J S I'd a media Social media Enabled ? Name Social media Version Description I ? J Example module to demonstrate module media. beta 11 Operations Help Permissions Configure Widgets Enabled Name Widgets Version Description Operations xl. a. Enables easy management of code snipes like Twitter, Facebook and Google buttons. betaS Required by: Widgets Service links (disabled) Widgets 7. x-I. 0- Enables links from Service links module to be used as widget elements Service links betaS Requires: Widgets (disabled), Service) inks (miSSing) hello on, matter ode export Key ords kill ? ion o. CHRIS SHATTUCK View Ed. 1- Shortcuts Social profiles SUBMIT File browser Devel HOW TO QUICKLY SET UP AN AWESOME, CUSTOMIZED FREE VOICE TESTIMONIAL SERVICE a ? f, '&lt;1 Mon. 2012-09-2421 :36 Chris Shattuck Ever since heard about the VoiP Drupal I've been itching to set up some kind of service that allows people to call in and leave testimonials for BuildAMocMe. But. haven't had chance and decided today that it was time to do something about it. ran into this awesome article outlining the process of using Google Voice to set up the service, and 1; I. it I. pH

Sep 07 2012
Sep 07

Back when I was at Optaros and CMIS was first showing up in Alfresco in draft form, we developed the Drupal CMIS module. We had a few customers interested in combining the two technologies but I think we were a few years ahead of our time. Now it seems I come across people wanting to combine the two nearly every week in IRC, the forums, or in internal discussions. Alfresco has contributed additional modules for Drupal integration. And multiple partners have full-fledged solutions or service offerings built on top of both.

Optaros has changed their web site a bit since those days and a couple of screencasts I recorded back then have been removed. Because so many people are still interested in this topic, I’ve posted them on YouTube, with Optaros’ permission (thanks!).

So, if you want to see some examples of Drupal and Alfresco working together, here are a couple of screencasts from the archive…

Drupal Plus Alfresco (Original post, 4/7/2009)

[embedded content]

Open Atrium Plus Alfresco (Original post, 10/13/2009)

[embedded content]

Drupal, Open Atrium, the CMIS modules, and Alfresco have all progressed since then, but the general gist is the same.

Thanks to Optaros for letting me make these available!

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