Wednesday, June 13, 2012

Does Google Index Text in Images?

This been a hotly debated issue for me recently, and thus I'm putting forward this test which I'll update the results of down the road.


Tuesday, April 10, 2012

Truly Responsive CSS Sprites

I've had this nagging issue for some time now with principles versus reality.  Mobile first is the future of design, so I've done my best to adopt responsive principals.  I also work as part of an SEO team, so those principals are incredibly important as well.  One of the things that keeps me up at night is the use of small images on designs.  Designers want them all over the place, SEO says performance matters (a lot!), so the long held solution was sprite maps.  But, sprite maps help performance but have typically been implemented using background images, so we lose out on the juicy SEO value of alt tags and the actual value of the img tag itself.  Possible solution?  Just use the img tag and css clip.  Now comes along responsive design, and I've seen a number of partial solutions that don't satisfy all of the previously mentioned needs, hence this post.  I've put together a solution that satisfies all of the above conditions with some basic css trickery.

The approach can be detailed as such, create your sprite map, use a wrapping div with hidden overflow, and then use an img tag with nothing but percentage based widths and margins and you have yourself a responsive SEO friendly sprite map.  One important detail that I have experienced is that as the image scales to very small levels, then top and bottom percentages get a little off.  One potential workaround if this is a problem is to add some transparent padding at the top and bottom of each block.  My example does not, so you will see a little bit of sprite bleeding.  Now the html / css:


<div class="responsive-sprite" style="width: 5%;">
<img alt="Yay for alt tags..." src="your-pic.png" />
</div>
<style>
img {
 width: 100%;
 margin-top: -96%;
 margin-bottom: -391%;
}
.responsive-sprite {
 overflow: hidden;
}
</style>

Which produces the result:

Yay for alt tags


As always questions and feedback are welcomed and I'll be sure to update with anything else I learn!

UPDATE: After several comments and time/help from a coworker.  We were able to figure out a computational way to determine the percentages.  It turns out that it is:

 (sprite-height / sprite-width) * -100 = Base Percentage

Then for margin-top, it is the (nth - 1 sprite) * Base Percentage.

The for margin-bottom it is (sprite_count - nth_sprite) * Base Percentage.

Hope that works for everyone!

Thursday, December 22, 2011

Support Migrated Content with Redirects

I'm returning from my hiatus for a quick blurb discussing how to support all of those backlinks to old content that you might be migrating into Drupal.  The scenario: you have an old cms that your content was in and you are moving to Drupal.  Perhaps that old cms injected its tech base into your extensions and you used to have a bunch of articles like "asdf/this-article-is-pretty-sweet.aspx" or "kjhgg/asfjhsf/we-have-like-the-best-writers.fubar" and you have come to your senses and have decided that for proper SEO I want all of my articles in Drupal to look like articles/real pages to those bots with a valid web page extension and will instead end with .htm.  Maybe for SEO reasons, you are completely retooling your url structure and want something that makes sense for your new setup.

These are all valid scenarios and it is recommended to think about those urls especially when migrating and the option is on the table.  Just don't forget, you want to preserve those old dead links and provide a redirect to get  all those back links to the right place.  One approach could be to generate a ton of mod-rewrite lines and handle it at that level, that could be one option.  You are already writing a process to migrate those articles, and hopefully you are using the Migrate Module?  If not, seriously consider it.

Now that you've built your article migration out, you are so very close to making all of those backlinks redirect.  First, go out and get the Redirect module, which works with the Global Redirect module, which you should already have if you are concerned with SEO.  

Next, provide the existing alias for the content as a field (I'll call it ContentURLAlias) in your migration, but there is no need to map it.

The next step is to expand on the content migration and override the complete function.  When we do this, we will call the redirect methods to take the new path of the entity that has just been saved back to the database, and the alias passed in and create our redirect like so:


  public function complete($entity, stdClass $row) {
      $redirect = new stdClass();
      redirect_object_prepare($redirect, array(
          'source' => $row->ConentURLAlias,
          'source_options' => array(),
          'redirect' => $entity->uri['path'],
          'redirect_options' => array(),
          'language' => LANGUAGE_NONE,
        ));
      redirect_save($redirect);
  }

And that is it!  Along with the redirect module you'll get tracking statistics on these old backlinks so you can recall when they get stale and decide when the safest point would be to clean them up or retain them permanently.

I hope this was helpful!

Tuesday, October 4, 2011

Add Permissions to Workbench Moderation States in Drupal

I've been attempting to implement a workflow solution for my Drupal 7 project, and at this time the best option to me is the Workbench module:

http://drupal.org/project/workbench

Props to those guys for what I see as a good implementation.  The breakout of functionality into separate modules is quite nice.  It helps keep down on the bloat you might experience when all you might need is a few simple options.

Now I needed to implement a pretty simple workflow with a few states and various roles for the users who will be interacting with the content in those states.  My needs were almost met entirely with the Workbench and Workbench Moderation modules.  My Achilles heel lied with a basic requirement of limiting the permissions of some roles depending on what state the node was in.

The answer wasn't too hard, but I thought it would be useful for someone out there in what would be a common scenario.  What I like about this approach is that it makes the permission settings easy to export via the Features module and takes advantage of the existing permissions interface to get setup.  The biggest drawback is that it potentially adds to permissions sprawl.

All of my code is included in my own custom module so you don't need to modify any of the code within the Workbench modules.  The first step is to implement hook_permissions to add a permission setting for each state in your workflow:


function mymodule_permission() {
  $permissions = array();
  
  // Per state permissions.  Used by workbench_moderation_node_access().
  $states = workbench_moderation_states();
  foreach($states as $state) {
    $permissions['edit all content in ' . $state->name] = array(
        'title' => t('Edit all content in @state', array('@state' => $state->label)),
    );
    if (variable_get('workbench_moderation_per_node_type', FALSE)) {
      foreach ($node_types as $node_type) {
        $permissions["edit $node_type in " . $state->name] = array(
          'title' => t('Edit @node_type in @state', array('@node_type' => node_type_get_name($node_type), '@state' => $state->label)),
        );
      }
    }
  }
  return $permissions;
}

Note the extra code that supports the functionality that opens up different worfklow settings in Workbench for your content types.  Your implementation might not need this, but I wanted to be thorough.

Next we need to implement hook_node_access to potentially deny access to update operations on a node based on these access settings, again taking into account the per content type variable:


function mymodule_permission() {
  if(isset($node->workbench_moderation) && $op == 'update') {
    if(variable_get('workbench_moderation_per_node_type', FALSE) &&
          !user_access('edit ' . $node->type . ' in '.$node->workbench_moderation['current']->state, $account)) {
      return NODE_ACCESS_DENY;
    }
    else if(!variable_get('workbench_moderation_per_node_type', FALSE) &&
          !user_access('edit all content in '.$node->workbench_moderation['current']->state, $account)) {
      return NODE_ACCESS_DENY;
    }
  }  
  return NODE_ACCESS_IGNORE;
}

Here we make sure to return NODE_ACCESS_IGNORE if none of our cases match so we don't accidentally grant access or deny access for other modules are able to jump in and have their say.

That's all for this post and as always feel free to leave any questions!

Tuesday, September 20, 2011

Drupal Migrate Multiple Taxonomy Terms

I stumbled upon a blog post yesterday discussing a way to import a node with multiple terms tied it:

http://stellapower.net/blog/migrate-module-migrating-nodes-taxonomy-terms

I wanted to offer up an alternative approach that I've utilized that hopefully is easier to use without using hook_migrate_prepare_node.  I will start by assuming that our source data has a table I'll call node, with a 1 to many relationship with a term table.  From there, let's build a simple query object with just a couple of fields (not all that you would need, just an example).


$subquery = db_select('term', 't2');
$subquery->join('node', 'n2', 't2.nid=n2.nid');
$subquery->groupBy('t2.nid');
$subquery->addField('t2', 'nid');
$subquery->addExpression("GROUP_CONCAT(t2.label SEPARATOR '|')", 'labels');

$query = db_select('node', 'n')
	 ->fields('n', array('nid'));
$query->join($subquery, 't', 'n.nid=t.nid');
$query->addField('t', 'labels');

This example uses the built in pdo objects to build the select and then we can use mysql's GROUP_CONCAT function to build a column containing multiple values.  The migrate module the has built in support for separators via a function on a field mapping:

$this->addFieldMapping('field_terms', 'labels')
     ->separator('|');

I've been doing migrations from MS Sql Server, so if you are in that world you might be saying "we don't have group_concat".  True, but we can slap a view with a little hack to do the concat for us and do a select from Drupal through that view.  In my case we'll use the '!' character as a space replacement to preserve the original whitespace.  With the xml path hack, it will become space separated which we will convert to '|' and then convert the '!' back to whitespace.

SELECT * FROM node n
LEFT OUTER JOIN (
	select t.nid, 
		REPLACE(REPLACE(RTRIM((
			SELECT CAST(REPLACE(dat.DrupalTerm, ' ', '!') AS VARCHAR(MAX)) + ' ' 
			FROM term t2 WITH (nolock)
			WHERE t2.tid = t.tid FOR XML PATH (''))), ' ', ',')
			, '!', ' ') AS labels
	from term t WITH (nolock)
	GROUP BY t.nid
) my_terms ON n.nid = my_terms.nid

As always, let me know if you have any questions!

Friday, September 16, 2011

How To Install Drupal 7 on Centos/Fedora/RedHat

Recently I have experienced some pain in getting drupal going on a CentOS server. I had done all of my work based on the drupal ubuntu quickstart, so this was a bit of a painful transition. The following blog post proved to be extremely helpful:


However, it didn't get me 100% of the way. I got through the installation, however I was utilizing drush to manage the acquisition of all of my modules. Going down this route, it was very confusing when I could download my module, enable my module, yet some features were available through the UI, but they wouldn't show up on my module page and they wouldn't show up on the configuration page, and any time you tried to access pages related to the module the page would error out. In my server logs was a key clue:

PHP Fatal error: require_once(): Failed opening required '/var/www/html/sites/all/modules/features/features.admin.inc' (include_path='.:/usr/share/pear:/usr/share/php') in /var/www/html/includes/menu.inc on line 501

At this point, my frustration perhaps blinding me from the answer in front of my face. Looking through the installation steps, there were two steps that needed to be rerun anytime I was using drush to dl a new module:

chown -R apache.apache sites/all/modules/

chcon -R -t httpd_sys_content_rw_t sites/all/modules/

Once I reran these commands after each download I was good to go.

Tuesday, August 30, 2011

Migrate Module and Important SEO Data

Currently, the modules for some important SEO info are in a bit of flux, so at the time that this article is being written I have chosen to go with two modules, Page Titles and Metatags Quick:


Assuming that you have already setup the modules, and for metatags you have enabled a field (my case will be the description), I will be focusing on getting data into these modules' fields using the Migrate Module.

Unfortunately, it doesn't appear (to me at least) that the Page Title data is exposed as a field in association with your node. So, my solution was to create an entire Migration class solely for the Page Title. If you take a look at the Migrate Extras module, it only supports Page Title as a destination in 6.x. This isn't a big deal for us because it is easy to deduce that Page Title is storing all of it's data in a table, so we can use the MigrateDestinationTable specifying the 'page_title' table:

$this->destination = new MigrateDestinationTable('page_title');

Unfortunately, the automatic deduction of the destination schema was giving me some hiccups which resulted in Migrate telling my that it couldn't find the column destid1 during the import. If you check the map tables for this migration you'll see it is completely missing any destination columns. So, my schema definition looks a little something like:

     

     array('type' => array('type' => 'varchar',

                           'length' => 15,

                           'not null' => TRUE,

                           'description' => 'Page Title Module Entity Type'),

           'id' => array('type' => 'int',

                         'unsigned' => TRUE,

                         'not null' => TRUE,

                         'description' => 'Page Title Entity ID',                       

             ))

The rest is pretty straightforward at this point. Now we have the page title ready to go, the next thing I wanted to include was the description metatag.

For this, I created a MigrateFieldHandler that does all the heaving lifting for me. Below is the code I utilized for the metatag field:

class MigrateMetaTagsQuickFieldHandler extends MigrateFieldHandler {

 public function __construct() {

   $this->registerTypes(array('metatags_quick'));

 }



 public function prepare($entity, array $field_info, array $instance, array $values) {

   $arguments = array();

   $language = $this->getFieldLanguage($entity, $field_info, $arguments);



   // Setup the standard Field API array for saving.

   $delta = 0;

   foreach ($values as $value) {

     $item = array();

     $item['metatags_quick'] = $value;



     $return[$language][$delta] = $item;

     $delta++;

   }



   return isset($return) ? $return : NULL;

 }

}

As usual, feel free to hit me up if you have any questions!

Update: This is now available via patch in an issue for the migrate extras module:

http://drupal.org/node/1264506

Update #2: After running this migration against production data, it turns out that we have some unfortunately long meta descriptions in the source data.  For my purposes, I'll get the data cleaned up after moving to Drupal, but in the meantime I needed to modify the length of the field in the Metatags Quick module, which is defaulted to 255 w/o any options for modification.  I have since created a patch that grants access to a max_length setting in an issue comment thread:

http://drupal.org/node/1272584#comment-5008562