First Draft at The New York Times + WordPress

Screen Shot 2014-09-29 at 4.00.56 PM

Last week, we launched a new vertical for politics at the Times: First Draft. First Draft is a morning newsletter/politics briefing and a web destination that is updated throughout the day by reporters at the Times’ Washington bureau.

First Draft is powered by WordPress. As I have noted previously, the Times runs on a variety of systems, but WordPress powers all of its blogs. “Blogs” have a bad connotation these days – not all things that have an informal tone and render posts in reverse-chronological order are necessarily a blog, but anything that does that should probably be using WordPress. WordPress has the ability to run any website, which is why it powers 23% of the entire internet. We are always looking for projects to power with WordPress at the Times, and First Draft was a perfect fit.

The features I am going to highlight that First Draft takes advantage of are things we take for granted in WordPress. Because we get so much for free, we can easily and powerfully build products and features.

Day Archives

First Draft is made up of Day archives. The idea is: you open the site in a tab at the beginning of the day, and you receive updates throughout the day when new content is available. A lot of the code that powers the New York Times proper is proprietary PHP. For another system to power First Draft, someone would have to re-invent day archives. And they might not think to also re-invent month and year archives. You may laugh, but those conversations happen. Even if the URL structure is present in another app, your API for retrieving data needs to be able to handle the parsing of these requests.

Day archives in WP “just work” – same with a lot of other types of archives. We think nothing of the fact that pretty URLs for hierarchical taxonomies “just work.” Proprietary frameworks tend to be missing these features out of the box.

Date Query

When WP_Date_Query landed in WordPress 3.7, it came with little fanfare. “Hey, sounds cool, maybe I’ll use it someday…” It’s one of those features that, when you DO need it, you can’t imagine a time when it didn’t exist. First Draft had some quirky requirements.

There is no “home page” per se, the home page needs to redirect to the most current day … unless it’s the weekend: posts made on Saturdays and Sundays are rolled up into Friday’s stream.

Home page requests are captured in 'parse_request', we need to find a post what was made on a day Monday-Friday (not the weekend):

$q = $wp->query_vars;

// This is a home request
// send it to the most recent weekday
if ( empty( $q ) ) {
    $q = new WP_Query( array(
        'ignore_sticky_posts' => true,
        'post_type' => 'post',
        'posts_per_page' => 1,
        'date_query' => array(
            'relation' => 'OR',
            array( 'dayofweek' => 2 ),
            array( 'dayofweek' => 3 ),
            array( 'dayofweek' => 4 ),
            array( 'dayofweek' => 5 ),
            array( 'dayofweek' => 6 ),
        )
    ) );

    if ( empty( $q->posts ) ) {
        return;
    }

    $time = strtotime( reset( $q->posts )->post_date );
    $day = date( 'd', $time );
    $monthnum = date( 'm', $time );
    $year = date( 'Y', $time );

    $url = get_day_link( $year, $monthnum, $day );
    wp_redirect( $url );
    exit();
}

If the request is indeed a day archive, we need to make sure we aren’t on Saturday or Sunday:

$vars = array_keys( $q );
$keys = array( 'year', 'day', 'monthnum' );

// this is not a day query
if ( array_diff( $keys, $vars ) ) {
    return;
}

$time = $this->vars_to_time(
    $q['monthnum'],
    $q['day'],
    $q['year']
);
$day = date( 'l', $time );

// Redirect Saturday and Sunday to Friday
$new_time = false;
switch ( $day ) {
case 'Saturday':
    $new_time = strtotime( '-1 day', $time );
    break;
case 'Sunday':
    $new_time = strtotime( '-2 day', $time );
    break;
}

// this is a Saturday/Sunday query, redirect to Friday
if ( $new_time ) {
    $day = date( 'd', $new_time );
    $monthnum = date( 'm', $new_time );
    $year = date( 'Y', $new_time );

    $url = get_day_link( $year, $monthnum, $day );
    wp_redirect( $url );
    exit();
}

In 'pre_get_posts', we need to figure out if we are on a Friday, and subsequently get Saturday and Sundays posts as well, assuming that Friday is not Today:

$query->set( 'posts_per_page', -1 );

$time = $this->vars_to_time(
    $query->get( 'monthnum' ),
    $query->get( 'day' ),
    $query->get( 'year' )
);
$day = date( 'l', $time );

if ( 'Friday' === $day ) {
    $before_time = strtotime( '+3 day', $time );

    $query->set( '_day', $query->get( 'day' ) );
    $query->set( '_monthnum', $query->get( 'monthnum' ) );
    $query->set( '_year', $query->get( 'year' ) );
    $query->set( 'day', '' );
    $query->set( 'monthnum', '' );
    $query->set( 'year', '' );

    $query->set( 'date_query', array(
        array(
            'compare' => 'BETWEEN',
            'before' => date( 'Y-m-d', $before_time ),
            'after' => date( 'Y-m-d', $time ),
            'inclusive' => true
        )
    ) );
}

Screen Shot 2014-09-29 at 4.25.06 PM

Adjacent day navigation links have to be aware of weekend rollups:

function first_draft_adjacent_day( $which = 'prev' ) {
    global $wp;

    $fd = FirstDraft_Theme::get_instance();

    if ( ! is_day() ) {
        return;
    }

    $archive_date = sprintf(
        '%s-%s-%s',
        $wp->query_vars[ 'year' ],
        $wp->query_vars[ 'monthnum' ],
        $wp->query_vars[ 'day' ]
    );

    if ( $archive_date === date( 'Y-m-d' ) && 'next' === $which ) {
        return;
    }

    $archive_time = strtotime( $archive_date );
    $day_name = date( 'l', $archive_time );

    if ( 'Thursday' === $day_name && 'next' === $which ) {
        $time = strtotime( '+1 day', $archive_time );
        $day = date( 'd', $time );
        $monthnum = date( 'm', $time );
        $year = date( 'Y', $time );

        $before_time = strtotime( '+3 day', $time );

        $ids = new WP_Query( array(
            'ignore_sticky_posts' => true,
            'fields' => 'ids',
            'posts_per_page' => -1,
            'date_query' => array(
                array(
                    'compare' => 'BETWEEN',
                    'before' => date( 'Y-m-d', $before_time ),
                    'after' => date( 'Y-m-d', $time ),
                    'inclusive' => true
                )
            )
        ) );

        if ( empty( $ids->posts ) ) {
            return;
        }

        $count = count( $ids->posts );

    } elseif ( 'Friday' === $day_name && 'next' === $which ) {
        $after_time = strtotime( '+3 days', $archive_time );

        $q = new WP_Query( array(
            'ignore_sticky_posts' => true,
            'post_type' => 'post',
            'posts_per_page' => 1,
            'order' => 'ASC',
            'date_query' => array(
                array(
                    'after' => date( 'Y-m-d', $after_time )
                )
            )
        ) );

        if ( empty( $q->posts ) ) {
            return;
        }

        $date = reset( $q->posts )->post_date;
        $time = strtotime( $date );

        $day = date( 'd', $time );
        $monthnum = date( 'm', $time );
        $year = date( 'Y', $time );

        $ids = new WP_Query( array(
            'ignore_sticky_posts' => true,
            'fields' => 'ids',
            'posts_per_page' => -1,
            'year' => $year,
            'month' => $monthnum,
            'day' => $day
        ) );

        $count = count( $ids->posts );

    } else {
        // find a post with an adjacent date
        $q = new WP_Query( array(
            'ignore_sticky_posts' => true,
            'post_type' => 'post',
            'posts_per_page' => 1,
            'order' => 'prev' === $which ? 'DESC' : 'ASC',
            'date_query' => array(
                'relation' => 'AND',
                array(
                    'prev' === $which ? 'before' : 'after' => array(
                        'year' => $fd->get_query_var( 'year' ),
                        'month' => (int) $fd->get_query_var( 'monthnum' ),
                        'day' => (int) $fd->get_query_var( 'day' )
                    )
                ),
                array(
                    'compare' => '!=',
                    'dayofweek' => 1
                ),
                array(
                    'compare' => '!=',
                    'dayofweek' => 7
                )
            )
        ) );

        if ( empty( $q->posts ) ) {
            return;
        }

        $date = reset( $q->posts )->post_date;
        $time = strtotime( $date );
        $name = date( 'l', $time );

        $day = date( 'd', $time );
        $monthnum = date( 'm', $time );
        $year = date( 'Y', $time );

        if ( 'Friday' === $name ) {
            $before_time = strtotime( '+3 days', $time );

            $ids = new WP_Query( array(
                'ignore_sticky_posts' => true,
                'fields' => 'ids',
                'posts_per_page' => -1,
                'date_query' => array(
                    array(
                        'compare' => 'BETWEEN',
                        'before' => date( 'Y-m-d', $before_time ),
                        'after' => date( 'Y-m-d', $time ),
                        'inclusive' => true
                    )
                )
            ) );
        } else {
            $ids = new WP_Query( array(
                'ignore_sticky_posts' => true,
                'fields' => 'ids',
                'posts_per_page' => -1,
                'year' => $year,
                'month' => $monthnum,
                'day' => $day
            ) );
        }

        $count = count( $ids->posts );
    }

    if ( 'prev' === $which && $time === strtotime( '-1 day' ) ) {
        $text = 'Yesterday';
    } else {
        $text = first_draft_month_format( 'D. M. d', $time );
    }

    $url = get_day_link( $year, $monthnum, $day );
    return compact( 'text', 'url', 'count' );
}

WP-API (the JSON API)

The New York Times is already using the new JSON API. When we needed to provide a stream for live updates, the WP-API was a far better solution (even in its alpha state) than XML-RPC. I implore you to look another developer in the face and tell them you want to build a cool new app, and you want to share data via XML-RPC. I’ve done it, they will not like you.

We needed to make some tweaks – date_query needs to be an allowed query var:

public function __construct() {
    ...
    add_filter( 'json_query_vars', array( $this, 'json_query_vars' ) );
    ...
}

public function json_query_vars( $vars ) {
    $vars[] = 'date_query';
    return $vars;
}

This will allow us to produce urls like so:


<meta name="live_stream_endpoint" content="http://www.nytimes.com/politics/first-draft/json/posts?filter[posts_per_page]=-1&filter[date_query][0][compare]=BETWEEN&filter[date_query][0][before]=2014-09-29&filter[date_query][0][after]=2014-09-26&filter[date_query][0][inclusive]=true"/>

Good times.

oEmbed

We take for granted: oEmbed is magic. Our reporters wanted to be able to “quick-publish” everything. Done and done. oEmbed also has the power of mostly being responsive, with minimal tweaks needed to ensure this.

How many proprietary systems have an oEmbed system like this? Probably none. Being able to paste a URL on a line by itself and, voila, you get a YouTube video or Tweet is pretty insane. TinyMCE previews inline while editing is even crazier.

Conclusion

There isn’t a lot of excitement about new “blogs” at the Times, but that distaste should not be confused with WordPress as a platform. WordPress is still a powerful tool, and is often a better solution than reinventing existing technologies in a proprietary system. First Draft is proof of that.

2 thoughts on “First Draft at The New York Times + WordPress

  1. Fabulous example of why WP is awesome and the best write up I’ve ever seen of handling custom prev/next links for unusual conditions. Thanks!

  2. Interesting that I never noticed WordPress defaults internally defaults to Sunday as the first day of the week and outwardly defaults to Monday as the first day of the week in the settings. Granted, the former is for general PHP compatibility but wouldn’t be interesting (see: insane) if date_query honored the first day of the week setting?

Comments are closed.