Here’s the deal (sort of)…

OK, so here’s the deal (sort of) with those last couple of ambiguous posts. I’m trying to take advantage of some of the newest features of WordPress (which seem largely intended to keep WordPress relevant in a post-Tumblr world), especially the ability to create different post formats, which, in addition to the “standard” format, include asides, links, galleries, status(es), quotes, and images.

In order to get access to these new features, I’ve switched from my old, built-from-scratch custom theme to the current stock theme, Twenty Eleven, which I am currently modifying for my own nefarious purposes. (If you consider hot pink text in a whimsical retro font nefarious, which you should.) My goal is to get things fairly close to how they looked before the switch, while still gaining access of all of that new WordPress mojo.

Cool. But what I really want to do is to take all of this even a step further, and let this WordPress-based blog become my single hub for posting anything online, except I guess for photos, which I still plan to post through Instagram, because I like how the app works. (Although having those show up in the main blog content stream instead of, or in addition to, in a sidebar widget would also be nice.)

The biggest stumbling block for this grand vision, so far, is that Twitter Tools, the WordPress plugin I use for all of the Twitter integration (a.k.a. “twittergration” in my compulsive Twitter portmanteau, or “twortmanteau”, parlance) on the site, isn’t smart enough to handle these special formats in the way I’d like. It should recognize asides, and especially status(es), as such and just run them as the entirety of the tweet, without the usual “UoP:” prefix and permalink consuming precious characters.

Or, perhaps more rationally, the ability of Twitter Tools to turn tweets into posts should allow you to define the format of those tweet posts (“twosts”), so I could tell it to make all of my twosts into status(es) instead of “standard” posts. Yes, this is definitely a more rational approach, and one that makes me slightly embarrassed to have written the previous paragraph (but not enough to make me delete it). I’m very accustomed to tweeting on-the-go from my iPhone, and I’d prefer to keep using Tweetbot for that, instead of somehow trying to turn the WordPress app into my go-to tool for depositing random brain cruft onto the interwebs.

While I’m wishing for alternative methods of funneling content into WordPress, as I mentioned above it would also be super neato if I could get Instagram photos to automatically show up on the blog as image posts… which might be possible, if I were to take the time to investigate it, but one thing at a time.

The end result of all of this angsting is that my blog is currently not in a state that I intend for it to remain in for very long. It’s a work in progress (as is everything in the world that isn’t just being allowed to decay), and I suppose I can live with it for now. I have more important things to worry about at the moment, unless you’d care to make a generous donation for the ongoing care and feeding of my blog. (4 figures minimum, and that’s U.S. dollars… not pennies, wooden nickels or, um… “Star Bucks”)

Then again, maybe I’ll be able to think about all of this a little more rationally come Monday, when the Minneapolis Public Schools’ winter break is finally over.

How I migrated a client’s blog from Drupal to WordPress 3.0

I just finished migrating a client’s blog from Drupal to WordPress 3.0. When I took on the project, I assumed there was a straightforward way to migrate content from Drupal to WordPress, but I was mistaken. Fortunately, there is a way, but it involves directly mucking about with the Drupal and WordPress databases. Never fear! There are some tools to help.

After a bunch of research, I found the most comprehensive and up-to-date solution on Mike Smullin’s blog. It wasn’t that up-to-date, though, having been written over a year ago and targeted at WordPress 2.7. Some helpful comments on that post offered a few refinements, and I added a few of my own to accommodate the particular quirks of this client’s site.

Since Mike Smullin (and D’Arcy Norman and Dave Dash before him) was so kind to share his work, I thought I should do the same. And so, I present my further refined SQL script for migrating Drupal data to a WordPress 3.0 site.

# DRUPAL-TO-WORDPRESS CONVERSION SCRIPT

# Changelog

# 07.29.2010 – Updated by Scott Anderson / Room 34 Creative Services http://blog.room34.com/archives/4530
# 02.06.2009 – Updated by Mike Smullin http://www.mikesmullin.com/development/migrate-convert-import-drupal-5-to-wordpress-27/
# 05.15.2007 – Updated by D’Arcy Norman http://www.darcynorman.net/2007/05/15/how-to-migrate-from-drupal-5-to-wordpress-2/
# 05.19.2006 – Created by Dave Dash http://spindrop.us/2006/05/19/migrating-from-drupal-47-to-wordpress/

# This assumes that WordPress and Drupal are in separate databases, named 'wordpress' and 'drupal'.
# If your database names differ, adjust these accordingly.

# Empty previous content from WordPress database.
TRUNCATE TABLE wordpress.wp_comments;
TRUNCATE TABLE wordpress.wp_links;
TRUNCATE TABLE wordpress.wp_postmeta;
TRUNCATE TABLE wordpress.wp_posts;
TRUNCATE TABLE wordpress.wp_term_relationships;
TRUNCATE TABLE wordpress.wp_term_taxonomy;
TRUNCATE TABLE wordpress.wp_terms;

# If you're not bringing over multiple Drupal authors, comment out these lines and the other
# author-related queries near the bottom of the script.
# This assumes you're keeping the default admin user (user_id = 1) created during installation.
DELETE FROM wordpress.wp_users WHERE ID > 1;
DELETE FROM wordpress.wp_usermeta WHERE user_id > 1;

# TAGS
# Using REPLACE prevents script from breaking if Drupal contains duplicate terms.
REPLACE INTO wordpress.wp_terms
    (term_id, `name`, slug, term_group)
    SELECT DISTINCT
        d.tid, d.name, REPLACE(LOWER(d.name), ' ', '_'), 0
    FROM drupal.term_data d
    INNER JOIN drupal.term_hierarchy h
        USING(tid)
    INNER JOIN drupal.term_node n
        USING(tid)
    WHERE (1
         # This helps eliminate spam tags from import; uncomment if necessary.
         # AND LENGTH(d.name) < 50
    )
;

INSERT INTO wordpress.wp_term_taxonomy
    (term_id, taxonomy, description, parent)
    SELECT DISTINCT
        d.tid `term_id`,
        'post_tag' `taxonomy`,
        d.description `description`,
        h.parent `parent`
    FROM drupal.term_data d
    INNER JOIN drupal.term_hierarchy h
        USING(tid)
    INNER JOIN drupal.term_node n
        USING(tid)
    WHERE (1
         # This helps eliminate spam tags from import; uncomment if necessary.
         # AND LENGTH(d.name) < 50
    )
;

# POSTS
# Keeps private posts hidden.
INSERT INTO wordpress.wp_posts
    (id, post_author, post_date, post_content, post_title, post_excerpt,
    post_name, post_modified, post_type, `post_status`)
    SELECT DISTINCT
        n.nid `id`,
        n.uid `post_author`,
        FROM_UNIXTIME(n.created) `post_date`,
        r.body `post_content`,
        n.title `post_title`,
        r.teaser `post_excerpt`,
        IF(SUBSTR(a.dst, 11, 1) = '/', SUBSTR(a.dst, 12), a.dst) `post_name`,
        FROM_UNIXTIME(n.changed) `post_modified`,
        n.type `post_type`,
        IF(n.status = 1, 'publish', 'private') `post_status`
    FROM drupal.node n
    INNER JOIN drupal.node_revisions r
        USING(vid)
    LEFT OUTER JOIN drupal.url_alias a
        ON a.src = CONCAT('node/', n.nid)
    # Add more Drupal content types below if applicable.
    WHERE n.type IN ('post', 'page', 'blog')
;

# Fix post type; http://www.mikesmullin.com/development/migrate-convert-import-drupal-5-to-wordpress-27/#comment-17826
# Add more Drupal content types below if applicable.
UPDATE wordpress.wp_posts
    SET post_type = 'post'
    WHERE post_type IN ('blog')
;

# Set all pages to "pending".
# If you're keeping the same page structure from Drupal, comment out this query
# and the new page INSERT at the end of this script.
UPDATE wordpress.wp_posts SET post_status = 'pending' WHERE post_type = 'page';

# POST/TAG RELATIONSHIPS
INSERT INTO wordpress.wp_term_relationships (object_id, term_taxonomy_id)
    SELECT DISTINCT nid, tid FROM drupal.term_node
;

# Update tag counts.
UPDATE wp_term_taxonomy tt
    SET `count` = (
        SELECT COUNT(tr.object_id)
        FROM wp_term_relationships tr
        WHERE tr.term_taxonomy_id = tt.term_taxonomy_id
    )
;

# COMMENTS
# Keeps unapproved comments hidden.
# Incorporates change noted here: http://www.mikesmullin.com/development/migrate-convert-import-drupal-5-to-wordpress-27/#comment-32169
INSERT INTO wordpress.wp_comments
    (comment_post_ID, comment_date, comment_content, comment_parent, comment_author,
    comment_author_email, comment_author_url, comment_approved)
    SELECT DISTINCT
        nid, FROM_UNIXTIME(timestamp), comment, thread, name,
        mail, homepage, ((status + 1) % 2)
    FROM drupal.comments
;

# Update comments count on wp_posts table.
UPDATE wordpress.wp_posts
    SET `comment_count` = (
        SELECT COUNT(`comment_post_id`)
        FROM wordpress.wp_comments
        WHERE wordpress.wp_posts.`id` = wordpress.wp_comments.`comment_post_id`
    )
;

# Fix images in post content; uncomment if you're moving files from "files" to "wp-content/uploads".
# UPDATE wordpress.wp_posts SET post_content = REPLACE(post_content, '"/files/', '"/wp-content/uploads/');

# Fix taxonomy; http://www.mikesmullin.com/development/migrate-convert-import-drupal-5-to-wordpress-27/#comment-27140
UPDATE IGNORE wordpress.wp_term_relationships, wordpress.wp_term_taxonomy
    SET wordpress.wp_term_relationships.term_taxonomy_id = wordpress.wp_term_taxonomy.term_taxonomy_id
    WHERE wordpress.wp_term_relationships.term_taxonomy_id = wordpress.wp_term_taxonomy.term_id
;

# OPTIONAL ADDITIONS — REMOVE ALL BELOW IF NOT APPLICABLE TO YOUR CONFIGURATION

# CATEGORIES
# These are NEW categories, not in Drupal. Add as many sets as needed.
INSERT IGNORE INTO wordpress.wp_terms (name, slug)
    VALUES
    ('First Category', 'first-category'),
    ('Second Category', 'second-category'),
    ('Third Category', 'third-category')
;

# Set category names to title case (in case term already exists [as a tag] in lowercase).
UPDATE wordpress.wp_terms SET name = 'First Category' WHERE name = 'first category';
UPDATE wordpress.wp_terms SET name = 'Second Category' WHERE name = 'second category';
UPDATE wordpress.wp_terms SET name = 'Third Category' WHERE name = 'third category';

# Add categories to taxonomy.
INSERT INTO wordpress.wp_term_taxonomy (term_id, taxonomy)
    VALUES
    ((SELECT term_id FROM wp_terms WHERE slug = 'first-category'), 'category'),
    ((SELECT term_id FROM wp_terms WHERE slug = 'second-category'), 'category'),
    ((SELECT term_id FROM wp_terms WHERE slug = 'third-category'), 'category')
;

# Auto-assign posts to category.
# You'll need to work out your own logic to determine strings/terms to match.
# Repeat this block as needed for each category you're creating.
INSERT IGNORE INTO wordpress.wp_term_relationships (object_id, term_taxonomy_id)
    SELECT DISTINCT p.ID AS object_id,
        (SELECT tt.term_taxonomy_id
        FROM wordpress.wp_term_taxonomy tt
        INNER JOIN wordpress.wp_terms t USING (term_id)
        WHERE t.slug = 'enter-category-slug-here'
        AND tt.taxonomy = 'category') AS term_taxonomy_id
    FROM wordpress.wp_posts p
    WHERE p.post_content LIKE '%enter string to match here%'
    OR p.ID IN (
        SELECT tr.object_id
        FROM wordpress.wp_term_taxonomy tt
        INNER JOIN wordpress.wp_terms t USING (term_id)
        INNER JOIN wordpress.wp_term_relationships tr USING (term_taxonomy_id)
        WHERE t.slug IN ('enter','terms','to','match','here')
        AND tt.taxonomy = 'post_tag'
    )
;

# Update category counts.
UPDATE wp_term_taxonomy tt
    SET `count` = (
        SELECT COUNT(tr.object_id)
        FROM wp_term_relationships tr
        WHERE tr.term_taxonomy_id = tt.term_taxonomy_id
    )
;

# AUTHORS
INSERT IGNORE INTO wordpress.wp_users
    (ID, user_login, user_pass, user_nicename, user_email,
    user_registered, user_activation_key, user_status, display_name)
    SELECT DISTINCT
        u.uid, u.mail, NULL, u.name, u.mail,
        FROM_UNIXTIME(created), '', 0, u.name
    FROM drupal.users u
    INNER JOIN drupal.users_roles r
        USING (uid)
    WHERE (1
        # Uncomment and enter any email addresses you want to exclude below.
        # AND u.mail NOT IN ('test@example.com')
    )
;

# Assign author permissions.
# Sets all authors to "author" by default; next section can selectively promote individual authors
INSERT IGNORE INTO wordpress.wp_usermeta (user_id, meta_key, meta_value)
    SELECT DISTINCT
        u.uid, 'wp_capabilities', 'a:1:{s:6:"author";s:1:"1";}'
    FROM drupal.users u
    INNER JOIN drupal.users_roles r
        USING (uid)
    WHERE (1
        # Uncomment and enter any email addresses you want to exclude below.
        # AND u.mail NOT IN ('test@example.com')
    )
;
INSERT IGNORE INTO wordpress.wp_usermeta (user_id, meta_key, meta_value)
    SELECT DISTINCT
        u.uid, 'wp_user_level', '2'
    FROM drupal.users u
    INNER JOIN drupal.users_roles r
        USING (uid)
    WHERE (1
        # Uncomment and enter any email addresses you want to exclude below.
        # AND u.mail NOT IN ('test@example.com')
    )
;

# Change permissions for admins.
# Add any specific user IDs to IN list to make them administrators.
# User ID values are carried over from Drupal.
UPDATE wordpress.wp_usermeta
    SET meta_value = 'a:1:{s:13:"administrator";s:1:"1";}'
    WHERE user_id IN (1) AND meta_key = 'wp_capabilities'
;
UPDATE wordpress.wp_usermeta
    SET meta_value = '10'
    WHERE user_id IN (1) AND meta_key = 'wp_user_level'
;

# Reassign post authorship.
UPDATE wordpress.wp_posts
    SET post_author = NULL
    WHERE post_author NOT IN (SELECT DISTINCT ID FROM wordpress.wp_users)
;

# VIDEO – READ BELOW AND COMMENT OUT IF NOT APPLICABLE TO YOUR SITE
# If your Drupal site uses the content_field_video table to store links to YouTube videos,
# this query will insert the video URLs at the end of all relevant posts.
# WordPress will automatically convert the video URLs to YouTube embed code.
UPDATE IGNORE wordpress.wp_posts p, drupal.content_field_video v
    SET p.post_content = CONCAT_WS('\n',post_content,v.field_video_embed)
    WHERE p.ID = v.nid
;

# IMAGES – READ BELOW AND COMMENT OUT IF NOT APPLICABLE TO YOUR SITE
# If your Drupal site uses the content_field_image table to store images associated with posts,
# but not actually referenced in the content of the posts themselves, this query
# will insert the images at the top of the post.
# HTML/CSS NOTE: The code applies a "drupal_image" class to the image and places it inside a <div>
# with the "drupal_image_wrapper" class. Add CSS to your WordPress theme as appropriate to
# handle styling of these elements. The <img> tag as written assumes you'll be copying the
# Drupal "files" directory into the root level of WordPress, NOT placing it inside the
# "wp-content/uploads" directory. It also relies on a properly formatted <base href="" /> tag.
# Make changes as necessary before running this script!
UPDATE IGNORE wordpress.wp_posts p, drupal.content_field_image i, drupal.files f
    SET p.post_content =
        CONCAT(
            CONCAT(
                '<div class="drupal_image_wrapper"><img src="files/',
                f.filename,
                '" class="drupal_image" /></div>'
            ),
            p.post_content
        )
    WHERE p.ID = i.nid
    AND i.field_image_fid = f.fid
    AND (
        f.filename LIKE '%.jpg'
        OR f.filename LIKE '%.jpeg'
        OR f.filename LIKE '%.png'
        OR f.filename LIKE '%.gif'
    )
;

# Fix post_name to remove paths.
# If applicable; Drupal allows paths (i.e. slashes) in the dst field, but this breaks
# WordPress URLs. If you have mod_rewrite turned on, stripping out the portion before
# the final slash will allow old site links to work properly, even if the path before
# the slash is different!
UPDATE wordpress.wp_posts
    SET post_name =
    REVERSE(SUBSTRING(REVERSE(post_name),1,LOCATE('/',REVERSE(post_name))-1))
;

# Miscellaneous clean-up.
# There may be some extraneous blank spaces in your Drupal posts; use these queries
# or other similar ones to strip out the undesirable tags.
UPDATE wordpress.wp_posts
    SET post_content = REPLACE(post_content,'<p> </p>','')
;
UPDATE wordpress.wp_posts
    SET post_content = REPLACE(post_content,'<p class="italic"> </p>','')
;

# NEW PAGES – READ BELOW AND COMMENT OUT IF NOT APPLICABLE TO YOUR SITE
# MUST COME LAST IN THE SCRIPT AFTER ALL OTHER QUERIES!
# If your site will contain new pages, you can set up the basic structure for them here.
# Once the import is complete, go into the WordPress admin and copy content from the Drupal
# pages (which are set to "pending" in a query above) into the appropriate new pages.
INSERT INTO wordpress.wp_posts
    (`post_author`, `post_date`, `post_date_gmt`, `post_content`, `post_title`,
    `post_excerpt`, `post_status`, `comment_status`, `ping_status`, `post_password`,
    `post_name`, `to_ping`, `pinged`, `post_modified`, `post_modified_gmt`,
    `post_content_filtered`, `post_parent`, `guid`, `menu_order`, `post_type`,
    `post_mime_type`, `comment_count`)
    VALUES
    (1, NOW(), NOW(), 'Page content goes here, or leave this value empty.', 'Page Title',
    '', 'publish', 'closed', 'closed', '',
    'slug-goes-here', '', '', NOW(), NOW(),
    '', 0, 'http://full.url.to.page.goes.here', 1, 'page', '', 0)
;

A few notes and suggestions:

  • Use at your own risk. I’m just sharing the code I used. In fact, it’s not even exactly the code I used, since I had to generalize a few things for mass consumption. I haven’t even tested this modified version. I make no guarantees as to its reliability or applicability to your particular implementation.
  • Download the script below; don’t just copy-paste the code from the block above. I had to apply some HTML formatting to make it look right, and this might result in bad characters that would break the script if run as-is above.
  • Always work with a test copy of your database before altering live site content. In theory this script can be run over and over again cleanly, but it will erase whatever data was already in the WordPress database, and there will be no way to get it back unless you have a backup.
  • Read through the code carefully, especially all comments before running the script. Most likely you will need to make some changes to the code.
  • Good luck! If you find bugs or develop further improvements, please comment below!

Download the Script

Download zip file... Drupal-to-WordPress Migration Script
drupal-to-wordpress.sql.zip • 4.8 KB

Fun with CSS in WordPress

I just had an email exchange with an old friend and fellow web developer (and WordPress user) regarding some techniques for CSS trickery on home pages in WordPress themes. Up until this week, I had been running a version of my theme that just featured brief excerpts of articles on the home page. I was doing this by brute force in PHP, truncating the post text with the substr() function, and then cleaning things up using the strip_tags() function, which removes all HTML tags from a string.

It got the job done, but as he and I were discussing, it wasn’t pretty: it stripped out the “dangerous” stuff — that is, unclosed HTML tags (cut off during truncation) that would have screwed up the formatting of the rest of the page. But it also stripped out desirable styling (bold, italics, links) and paragraph breaks.

The ideal situation would be to have a way to show just the first two paragraphs of each post, retaining all of their original formatting. Of course, WordPress has a feature to handle this: if you put a <!--more--> comment tag in your post, your page template can truncate the post at that point, with a link to the single-post page to display the rest of the content. But I’ve never liked having to put that <!--more--> into my posts. I want a completely automated solution.

And then it hit me… this could be done with CSS. It took a little trial and error, but I came up with the following:

#content .entry p,
#content .entry h3
{
display: none;
}

#content .entry h2:first-child,
#content .entry h2:first-child + p,
#content .entry h2:first-child + p + p
{
display: block;
}

A few things to note:

  • This assumes that your entire loop is wrapped inside <div id="content">...</div>. You may need to come up with a specific ID to use just for this block in your index page, and be sure not to use that in your single-post page, or your posts will never appear in their entirety.
  • This also assumes that you’re using the WordPress convention of wrapping your posts in a pair of <div> tags with the attributes class="post" and class="entry" (though technically, class="entry" is the only one that matters here).
  • Your post title should be in an <h2> tag, immediately following <div class="entry">.
  • The first definition may need to be extended to include other HTML tags you want to hide on your index page. In this example, it’s only hiding content that is inside <p> or <h3> tags.
  • If you want to hide every HTML tag except the ones you explicitly specify, you could change the first block to #content .entry *, but keep in mind that will also remove styling like bold and italics, and it will remove links. Probably not what you want.

The specifics may vary depending upon how your WordPress theme is set up; I just know that with the way mine is set up, which pretty closely follows standard convention, this CSS worked to get the index page to list the posts and only show the first two paragraphs of each. (It also retained the images that I embed at the start of each post, and also retains any embedded video from YouTube or Vimeo, since — at least the way I insert them — those are not wrapped in <p> tags.

Note that all of the HTML content for each post is still loaded by the browser — we’re just using CSS to tell the browser not to show it on the page. This is not going to help with performance; it’s strictly aesthetic.

Twittegration (or… uh… something like that)

Yes, I should inaugurate this new feature with a profound, witty, or at least marginally purposeful post. Alas, I might as well just say “Hello world!”

This post is a test of a new Twitter integration I’m trying out, using the Twitter Tools plugin for WordPress. Here goes (hopefully not) nothing!

Update: Looks like it works. Now I just need to get my custom URL shortener integrated into it all.

The new site design, phase two

Lots to choose from...As I mentioned the other day, the initial launch of this new site design was just phase one — window dressing. Window dressing I happen to like a lot, but still… same old clunky underlying structure. But not anymore.

WordPress has a pretty rudimentary home page structure by default. Everything’s just *SPLAT* right out there on the home page. Sure, you can use the <!--more--> tag to trigger some automagic stuff with “Read more” links, but overall, it’s not too fancy. Which isn’t to say it’s bad. Now, the default page layouts for some other open source CMSes like Drupal or Joomla, on the other hand… yeah, they’re bad. (Or at least they were, the last time I bothered to care.)

So while WordPress out of the box doesn’t do much fancy stuff with the home page layout, it’s extensible enough that a crafty developer (or a well-read tinkerer) can do some pretty nifty stuff with it. And that was my goal with this new redesign: I’ve got hundreds of posts dating back over seven years now, and most are eternally buried in the archives. I’m hopeful this new approach will bring some of that older content to light, with the random articles list on the home page and the related articles list at the end of each article page.

I didn’t do it all alone… I had some help from a nifty article from Smashing Magazine: 10 Exceptional WordPress Hacks. In particular I made use of numbers 5 through 8 on this list… with some modification. Some of my changes were purely to suit my taste, but others were to improve the usability of the features and in at least one case to fix a bug in the provided sample code. It’s probably worth discussing the details here.

5. Display Related Posts Without A Plug-In

I set this up in single.php so it will appear at the end of each of my articles. I experimented a bit with matching both tags and categories, but I found (for reasons I didn’t dig deep enough to explain) that WP_Query() does well with either tags or categories, but not both.

And, most importantly, I found (or actually, SLP did) that this sample as given breaks comments on the page. Some commenters on the original post mentioned this problem too, along with its solution: you need to call wp_reset_query(); at the end, to tell WordPress to go back to working with the original post’s content.

I also modified the example to look at all of the tags associated with the post, not just the first (can’t really figure out why the original version did that), and tweaked the HTML/CSS output to suit my design. Here’s a simplified version of the code I’m running:

<?php
// Get related posts
$tags = wp_get_post_tags($post->ID);
if (!empty($tags)) {
  $tag_list = array();
  foreach ((array)$tags as $tag) {
    $tag_list[] = $tag->term_id;
  }
  $args = array(
    'tag__in' => (array)$tag_list,
    'post__not_in' => (array)$post->ID,
    'showposts' => 5,
    'caller_get_posts' => 1,
    'orderby' => 'date'
  );
  $related_posts = new WP_Query($args);
  if ($related_posts->have_posts()) {
    ?>
    <h2>Related Posts</h2>
    <ul>
      <?php
      while ($related_posts->have_posts()) {
        $related_posts->the_post();
        ?>
        <li>
          <a href="<?php the_permalink() ?>"
          rel="bookmark" title="<?php the_title_attribute(); ?>"><?php the_title(); ?></a>
        </li>
        <?php
      }
      ?>
    </ul>
    <?php
  }
  wp_reset_query();
}
?>

My version also uses a special truncation function I wrote to display a short excerpt of each post, not shown here. (And yes, eventually I am going to get syntax highlighting set up.)

6. Automatically Retrieve The First Image From Posts On Your Home Page

This one worked pretty well. I changed the function name to something a little less quirky, and I also added some code to verify that the image actually exists, instead of just looking for an <img src> and assuming everything’s OK. This involved inserting the following block of code into the function immediately before if (empty($first_img)) {:

// Check that file exists
if (!empty($first_img)) {
  // remove http/ https/ ftp
  $src = preg_replace("/^((ht|f)tp(s|):\/\/)/i", "", $first_img);
  // remove domain name from the source url
  $host = $_SERVER["HTTP_HOST"];
  $src = str_replace($host, "", $src);
  $host = str_replace("www.", "", $host);
  $src = str_replace($host, "", $src);
  if (!file_exists(ABSPATH . $src)) {
    unset($first_img);
  }
}

If some of that code looks familiar, that’s because I copied it straight out of the next item. If I were writing it myself, or bothering to rewrite it, I would swap out that slightly cumbersome-looking three lines of str_replace() with a single preg_replace() — although perhaps the original coder knows something I don’t, like that doing it this way is actually faster. It very well could be; I know regular expressions are significantly slower than straight-up string replacements.

7. Resize Images On The Fly

I’m using this in conjunction with all of the other items here, no big surprise. I left it mostly as-is, although I did make one small change. I know well the dangers of scaling images up — they look like crap, basically. But a little scaling up doesn’t hurt. At least, it’s much less glaring than having a big set of uniform-looking images marred by one image that’s slightly smaller than the rest. That happened to me as I was putting this together, so I tweaked the script to allow images to be enlarged up to 1.5 times their original size. In timthumb.php I changed lines 103-109 to be:

// don't allow new width or height to be more than 50% greater than the original
if( $new_width > $width * 1.5 ) {
  $new_width = $width * 1.5;
}
if( $new_height > $height * 1.5 ) {
  $new_height = $height * 1.5;
}

8. Get Your Most Popular Posts Without A Plug-In

There was something about the way this one was written that really bothered me. Maybe it’s just that I have a knee-jerk reaction to seeing SQL code appear directly in a page template. But mostly it was that this didn’t match #5 closely enough. Luckily I discovered along the way that regardless of whether you retrieved post data using WP_Query (which returns an object) or with the $wpdb->get_results() method (which returns an array), you can use the same post functions once you’ve called setup_postdata(). So I kept everything from this example up through that, and then I used my modified version of the output code from #5 and it worked like a charm.

One other thing I’d recommend changing: it’s kind of silly to have the if ($commentcount != 0) conditional in there. Much better to just put WHERE comment_count > 0 in the SQL. I also added a date range to the SQL, to keep the list dynamic. In my case, it’s only looking at the most popular posts over a 3-month range. More active blogs could shorten that time frame. My full query looks like this:

$popular = $wpdb->get_results("SELECT * FROM " . $wpdb->posts .
" WHERE post_date > '" . date('Y-m-d',mktime(0,0,0,date('n')-3,date('j'),date('Y'))) .
"' AND comment_count > 0 ORDER BY comment_count DESC LIMIT 4");

There may have been some other changes I made that were relevant here but I think I covered all of the major ones. The tips on the Smashing Magazine site were invaluable in kick-starting my overhaul of the home page. It was uncanny that I stumbled upon this page today, just as I was getting down to this task anyway. It saved me a ton of time. But it still kept me on my toes, since all of the so-called “hacks” required some additional hacks of my own to get them working, or at least, to get them working the way I wanted!

In defense of WordPress

WordPressThere’s a lot of negative talk circulating regarding the security attacks currently underway against outdated versions of WordPress. One of the most outspoken critics, not without cause, is one of my favorite bloggers: John Gruber of Daring Fireball.

That Gruber is loyal to Movable Type perhaps influences (despite his claims to the contrary) the tone of his assessment of the situation. And, I’m sure, my loyalty to WordPress influences my assessment of it as well. WordPress is not Apple, but I hold both in perhaps unduly high esteem.

That said, there are easy (or, at least, prudent) steps one can take to keep WordPress secure against this attack. Also, security is not the only (nor, dare I say, anywhere near the most important) factor in selecting a blogging platform. I’ve worked a fair bit with Movable Type, and while I can’t speak to the relative security of the two applications, I definitely can speak to their relative ease of use, and in that regard, I see no comparison: WordPress is surprisingly consistent and intuitive, given its open source nature and the large size of its developer community, whereas Movable Type seems to live in its own world where up is down, left is right, files are assets, and you need to rebuild the site every time you change anything. (Caching, if it’s even necessary, should be invisible to the user.) And then there’s the proprietary markup language.

It is unfortunate, and a weakness of the system, that WordPress has come under attack in this fashion. I’m glad that the latest version, at least, is immune to this exploit. But to dismiss WordPress because of this seems to grossly miss the point. And, debate this if you like, I do believe that if you’re not prepared to keep your installation updated, you shouldn’t be hosting the site yourself anyway. Use WordPress.com — it’s free, and it’s always up-to-date. The biggest victims here, I fear, are site owners who have relied upon an apathetic hosting provider to manage their system, and whose sites have been left vulnerable through no fault of their own.

All of the room34.com sites are running 2.8.4, and none has fallen victim to these attacks. But this incident did inspire me to take an action I had been neglecting — last night I dug into my httpd.conf file, shuffled a bunch of directories around on the server, and consolidated all five of the WordPress sites I’m running down onto a single installation of the software, so from now on I’ll only need to update once instead of five times. I probably could have migrated to WordPress MU, but it was an interesting experiment to take the approach I did, and it allowed me to avoid having to merge databases.

New Coltrane site launched

John Coltrane, Avant Garde Jazz and the Evolution of "My Favorite Things"
I’m pleased to announce the launch of a brand new version of my John Coltrane website.

This is the first step in an ongoing process of splitting my currently mammoth website into distinct, separate websites tailored to specific content areas. Basically, each of the top-level navigation items you see at the top of this page will eventually become its own site.

For now there’s not a lot of new content on the Coltrane site — the big “draw” is the redesign itself. But I’ve added a blog to the site, and eventually I’ll also be adding a multimedia section where I’ll be featuring audio and video clips. Check it out!

This site design was also an opportunity for me to test the effectiveness of my new Room 34 Baseline WordPress theme. Believe it or not, that barebones theme really is the foundation upon which the new Coltrane site is built. So it works!

This site also takes advantage of some cutting-edge web design features: it’s built with HTML 5 and the Blueprint CSS framework, and it uses the emerging @font-face CSS method to render text in a custom font. I am using the free Museo font family throughout the site.

On upgrading WordPress (and WordPress plugins) automatically over SSH/SFTP

For the most part, I love managing my own server. Even though it requires digging into the muck of Linux configuration files with my bare hands (so to speak), and if it goes down, I have no one to blame (or call on for help) but myself, it’s great to have full control and flexibility.

One downside I discovered as a WordPress user, however, is that the super-slick automatic upgrade feature of WordPress was broken on my server. WordPress only supports FTP and the (as I see it) somewhat pointless FTPS. Insecure as it is, my old host supported regular ol’ FTP, and that made WordPress upgrades painless.

There’s no way I’m going to implement FTP on my own server. It’s easy enough to install the package at the command line (really, it is), but I just see no reason to open myself up to the security risks. Granted, there aren’t really that many security risks (beyond one very big one — intercepted passwords) with a well-configured FTP server. But I don’t care to investigate the steps necessary to ensure an FTP server is well-configured.

The obvious choice is to use SFTP/SSH, but at first it looked to me as if WordPress simply didn’t support it. But as I’ve learned (and since proven with my own server), WordPress does support it if your PHP installation has the proper extensions installed. And here’s a guide to get you started.

Once your PHP install is upgraded to support SSH connections, the option will automatically become available in the WordPress upgrade tools, and it works perfectly!

The growing problem of registration spam in WordPress

WordPressNow, this is odd.

A few months back I wrote a plug-in for WordPress called RegisTrap. It’s beyond basic, and has one simple purpose: to block registration spam on my WordPress-based website.

Registration spam, for those of you who don’t know, is when a “bot” (a computer program written to seek out and exploit poorly-written web forms to send floods of spam email messages) signs up as a “user” on your site. These “users” can’t really do anything on the site, but they clutter up your database nonetheless.

I had a feeling that RegisTrap was really only going to work reliably if I kept it to myself. And I was right: after submitting it to the official WordPress plug-in repository, it eventually stopped working, as the bots adapted to avoid its “traps.” It might have happened eventually anyway, but I’m sure that the publicity it received from being in the repository, and the hundred or so people who downloaded it (many of whom, I suspect on reflection, were probably bot developers looking to dissect its workings), accelerated its demise.

As I announced here a few days ago, I turned RegisTrap off on my site, and I also turned off registration altogether. But that hasn’t stopped the flood of new bot registrations. There are 14 of them sitting there in my database right now (well, there were before I just deleted them), all added after I turned off the ability to register altogether.

I suppose, since the bots don’t actually visit the site and fill in the form, they just submit the right data directly to the right URL, whether it’s “browsable” or not, it doesn’t even really matter if your site is set up to reject registrations. Still, it’s a bit dismaying that WordPress is processing those registrations even with registration turned off. Apparently it stops at making the registration page inaccessible via links; it doesn’t actually turn off the code that processes registrations. Boo. Perhaps that should be my next plugin: “Stop All Registrations 4 Realz.”

But maybe I won’t call it that.

RegisTrap seems to be losing its effectiveness

I suspected this might happen once I released RegisTrap to the public. I had four new spam user registrations on my site when I checked it today (having last checked it maybe two or three days ago). Previously I’d only see about one a week with RegisTrap running.

It was bound to happen. The rules RegisTrap employs are fairly simple, and the “bots” are constantly being modified. I have no idea how many registrations RegisTrap has blocked in the time it’s been running — perhaps my next step in developing it is to add a logging feature. If there were only four (or even maybe a dozen or so) spam attempts on my site during this time period, then RegisTrap seems pretty ineffectual. But if it actually blocked a ton (metaphorically speaking) of spam registrations, then four sneaking through doesn’t seem so bad.

If anyone out there is using RegisTrap and cares to comment on ways I could improve it, let me know! Meanwhile, as time allows I am going to pursue the logging functionality, if only for my own edification. As valuable as the logging feature would be, it goes against the spirit of simplicity inherent in the plugin. I really don’t want to write anything to the database or filesystem.