Using WordPress to Create a Newsletter

On my Laravel News site I send out a weekly digest newsletter and I decided very early that I didn’t want some automated system of just grabbing all this weeks posts and sending those. However, I did want to automate as much as I could and in this tutorial I want to share how I setup WordPress to handle the creation of each weeks mailing.

Here is a list of my goals so you can see what I wanted to accomplish:

  • Have a browsable archive on the site
  • Have each issue on the site and under my control
  • Write in markdown with full control over content sections
  • Have a signup form on past mailings
  • Be able to easily take it and send it with Sendy

Browsable Archive

Let’s tackle this first and luckily WordPress makes this simple by utilizing custom post types. I threw the example from the codex into my functions.php and like magic it appeared in my admin:

add_action( 'init', 'digest_create_posttype' );
function digest_create_posttype() {
  register_post_type( 'digest',
    array(
      'labels' => array(
        'name' => __( 'Weekly Digest' ),
        'singular_name' => __( 'Digest' )
      ),
      'public' => true,
      'has_archive' => true,
      'rewrite' => array('slug' => 'digest'),
      'supports' => array( 'title', 'editor', 'wpcom-markdown' ),
    )
  );
}

If you notice the only thing out of the ordinary is the “supports” line. I prefer to write in markdown and that integrates markdown from JetPack.

Archive Integrated with the site

Now that I have a custom post type its time to integrate it into the site. Since I named my post type “digest” I needed to create two new template files: “archive-digest” for the listing and “single-digest” for the individual mailing.

The archive-digest works with the normal WordPress post loop and I just have it show a simple list. I will not go through that code in this tutorial but you can see it on GitHub. Here is the parsed result:

digest

The single-digest is a different story. Because this will be used in email it needs to be formatted completely different from the site. Old school tables, inline css, no JavaScript, and bare basics. What I did for this template was remove wp_head() and wp_footer(). Those add additional code from plugins, and from the core, which isn’t needed for this.

Here is a trimmed down version of the header:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta name="viewport" content="width=device-width" />
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title><?php wp_title('|', true, 'right'); ?></title>
  <style type="text/css">
    <?= include(get_template_directory().'/newsletter.css') ?>
  </style>
</head>

The one thing to note here is how I’m including css inline. By utilizing php’s include() function I import the entire contents of newsletter.css right into the output. That will be used later by the css inliner service.

The body portion is again the typical WordPress post loop.

<!-- BODY -->
<?php while ( have_posts() ) : the_post(); ?>
    <?php the_content(); ?>
<?php endwhile; ?>

At this point I created a test digest post and it is indeed showing correctly. So now it’s time to move on to how to control the content blocks.

Controlling the Content Blocks

My digest template as you can see from the first screen shot is a single column with three or four sections broken up by a line. Each one is its own table and via css I apply a border-top to the content sections table. Primarily each section is custom written except for a listing of the weeks posts. So I thought I need a simple way to accomplish this without having to write tables in the admin. Tables are gross and I want to avoid that all costs.

I came up with the solution of creating shortcodes. Since I’m writing in markdown it makes sense to just add a manual “break” wherever I want. Here is an example of my markdown post content:

## Welcome
//...

[[break]]

## Section 2
//...

[[break]]

## Section 3
//...

The question is, what should [[break]] do? I want each block in a table so it needs to wrap it but I can’t grab all the content between the breaks as that would be messy. Instead I decided that a [[break]] would just close the table and open a new one. This will work as long as you don’t end the content with [[break]]. Seeing how this is not something for the masses I’m good with that.

To setup the shortcode I created a new file in my theme directory, “inc/digest.php” and included it from my main functions.php file. This way the main file doesn’t get out of control. Inside “inc/digest.php” I start adding my shortcode:

function laravelnews_table_start() {
  return '<table><tr><td class="container" bgcolor="#FFFFFF">';
}

function laravelnews_table_end() {
  return '</td></tr></table>';
}

function laravelnews_break() {
  return laravelnews_table_end().laravelnews_table_start();
}
add_shortcode('break', 'laravelnews_break');

So with this set, [[break]] in my post calls the “laravelnews_break” function which in turns calls table_end and table_start.

With that in place we still have a problem. Before our content is printed on the screen we need an opening table and after it needs to close it. So go back to single-digest.php and replace our content with this:

<!-- BODY -->
<?php while ( have_posts() ) : the_post(); ?>

  <?= laravelnews_table_start(); ?>
    <?php the_content(); ?>
  <?= laravelnews_table_end(); ?>

<?php endwhile; ?>

It just reuses the start and end table functions that we built in inc/digest and keeps it clean.

This weeks posts

One final section is I want to show all the posts from the past week. So I utilized another custom shortcode named [[digest_posts]]. Here is the code that will generate our list:

function laravelnews_posts_by_week() {
  $out = '';
  $the_query = new WP_Query( 'year=' . get_post_time( 'Y' ) . '&w=' . get_post_time( 'W' ) );
  if ( $the_query->have_posts() ) {
    $out .= '<ul>';
    while ( $the_query->have_posts() ) {
      $the_query->the_post();
      $out .= '<li><a href="'. get_the_permalink() .'">'. get_the_title() .'</a></li>';
    }
    $out .= '</ul>';
    wp_reset_postdata();
  }
  return $out;
}
add_shortcode('digest_posts', 'laravelnews_posts_by_week');

The magic here is the custom WP_Query. I pass in the year and week of the current post. Since I write these once a week it will always need the posts from the week the newsletter is published.

Now my finished newsletter copy looks like this:

## Welcome
//...

[[break]]

## This weeks posts

[[digest_posts]]

[[break]]

## Section 3
//...

Have a signup form on past mailings

Now that we have a basic layout with all the pieces in place I want to allow readers of this page to subscribe to the newsletter, but only show it if they are reading via the web not in what is sent in the email.

A simple solution to this is to use the built in “is_user_logged_in” WordPress function. Here is an example:

<?php if ( ! is_user_logged_in()): ?>
      //..
<?php endif; ?>

I put the newsletter form inside this if and now it only shows to those not logged in. Which in my case is everyone but myself.

Sending the Email

After everything above is completed now its time to do the manual processing. I’m using sendy.co (shameless affiliate link) to handle the emailing and it does have its own web preview. I prefer to have it on the blog and integrate my own features such as a signup form, analytics, etc.

I write the newsletter copy inside WordPress admin and hit preview until I’m satisfied. Next view source on the preview, and paste it into an inliner, finally take that and paste it into a new campaign in Sendy, schedule it, and head off to play golf or something.

Wrap Up

As with anything in the crazy world of web development this is just one solution to a problem. I’m sure you could use any of the plethora of mail services and I would recommend that in most cases. I enjoy thinking of obscure solutions like this and I like learning new tools. Building things like this is fun and by sharing this tutorial I hope you can at least pick up a few tips and tricks.

I also have a complimentary repo with all the code used in this tutorial.