Strattic Developer Docs

How to Add Breaking News Updates Instantly to Your Site

Looking for instant post updates without republishing or refreshing a page? Check this out 😎…

The idea behind this came to us from talking with several large news agencies and online publishers. This is definitely a proof of concept, but the idea here is that you can:

  1. Create a Breaking News item.
  2. Publish it quickly with Selective Publish on Strattic (should take less than one minute).
  3. Continue to update the content without needing to republish the post.
  4. Have the content update on the front end of the site instantly.

It would look something like this (editing on the left, instant updates on the right):

Pretty nifty!

Cool, no republish and no refreshing required 😎.

Doing this requires a plugin setup on your WordPress and to set up the serverless service with the Serverless framework.

Setup the Service

This will be your serverless.yml file:

org: your-org
app: byol-breaking-news
service: byol-breaking-news

frameworkVersion: '2'

provider:
  name: aws
  runtime: nodejs12.x
  lambdaHashingVersion: '20201221'
  environment:
      DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"


functions:
  create:
    handler: create.create
    events:
      - http:
          path: breaking-news/
          method: post
          cors: true
  get:
    handler: get.get
    events:
      - http:
          path: breaking-news/{id}
          method: get
          cors: true
plugins:
  - serverless-offline

resources:
  Resources:
    TodosDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.DYNAMODB_TABLE}

Then you will have two JS files you need to create (they are referenced above in the YAML file.

This is your create.js file:

"use strict";

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.create = (event, context, callback) => {
  console.log('got this:')
  console.log(event.body);
  const timestamp = new Date().getTime();
  const data = JSON.parse(event.body);
  console.log(data);
  if (typeof data.content !== 'string') {
    console.error('Validation Failed');
    callback(null, {
      statusCode: 400,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Couldn\'t create the todo item.',
    });
    return;
  }

  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Item: {
      id: data.eventId,
      content: data.content,
      createdAt: timestamp,
      updatedAt: timestamp,
    },
  };

  // write the todo to the database
  dynamoDb.put(params, (error) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t create the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(params.Item),
    };
    callback(null, response);
  });
};

This is your get.js file:

'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.get = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };

  // Fetch breaking news item from the database
  dynamoDb.get(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn\'t fetch the breaking news item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      headers: {
        'Access-Control-Allow-Origin': '*',
      },
      body: JSON.stringify(result.Item),
    };
    callback(null, response);
  });
};

Once you have these included in your service, you can run sls deploy or serverless deploy and get back a GET and a POST endpoint for your get and create services respectively.

Setup the Plugin

The files below will be a WordPress plugin, and you’ll need to replace the GET and POST endpoints you received from setting up the service and put them into the appropriate files. For the files below, your plugin directory should have this structure:

/breaking-news-serverless-plugin
|- index.php
|- breaking-news.js
|-/post-types
  |-breaking-news-post-type.php

This is your index.php file (include the POST endpoint in the wp_remote_post function):

<?php

/**
 * Plugin Name: Breaking News Block and Serverless Plugin
 * Plugin URI: https://github.com/stratticweb
 * Description: A serverless breaking news implementation.
 * Author: Strattic
 * Author URI: https://strattic.com/
 * Version: 1.0.0
 * License: GPL2+
 * License URI: https://www.gnu.org/licenses/gpl-2.0.txt
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// Add in the Breaking News custom post type.
require_once plugin_dir_path( __FILE__ ) . 'post-types/breaking-news-post-type.php';

/**
 * Sends the post content to our breaking news service.
 *
 * @param int $post_id The ID of the post.
 * @param object $post The post object
 * @param bool $update
 * @return void
 */
function send_news_item_to_serverless( $post_id, $post, $update ) {
	$args = wp_json_encode(
		array(
			'eventId' => "$post_id",
			'content' => $post->post_content,
		)
	);

	// Currently, we're not doing anything with this result.
	// This is the POST endpoint you get from your serverless service.
	$result = wp_remote_post(
		'https://1234567.execute-api.us-east-1.amazonaws.com/dev/breaking-news',
		array(
			'body' => $args,
		)
	);
}
add_action( 'save_post_breaking-news', 'send_news_item_to_serverless', 10, 3);


/**
 * Enqueue the scripts we need.
 *
 * @return void
 */
function enqueue_breaking_news() {
	wp_enqueue_script( 'breaking-news', plugin_dir_url( __FILE__ ) . '/breaking-news.js', array(), false, true );
}
add_action( 'wp_enqueue_scripts', 'enqueue_breaking_news' );


/**
 * Filter breaking news content.
 *
 * @param string $content The post type's content.
 * @return string
 */
function filter_breaking_news_content( $content ) {
	if ( is_singular( 'breaking-news' ) ) {
		global $post;
		return '<div data-post-id="' . $post->ID . '" class="breaking-news-container"></div>';
	}

	return $content;
}
add_filter( 'the_content', 'filter_breaking_news_content' );

This is your breaking-news.js file (include the GET endpoint in the fetch function:

function refreshFeed( newsElem ) {
    // Insert your GET endpoint here that you received from your serverless service.
    fetch('https://12345678.execute-api.us-east-1.amazonaws.com/dev/breaking-news/' + newsElem.dataset.postId)
    .then(response => response.json())
    .then(data => {
        const timestamp = new Date(data.updatedAt);
        newsElem.innerHTML = data.content + '<p style="color: grey;"><em>Last updated: ' + timestamp + '</em></p>';
        
        setTimeout( function() {
            refreshFeed(newsElem);
        }, 3000 );
    });
}
const breakingNewsContainers = document.getElementsByClassName('breaking-news-container');

for( const newsElem of breakingNewsContainers ) {
  refreshFeed(newsElem);
}

This is the breaking-news-post-type.php file (which registers the Breaking News post type):

<?php
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Register a custom post type called "breaking new".
 *
 * @see get_post_type_labels() for label keys.
 */
function breaking_news_cpt_init() {
    $labels = array(
        'name'                  => _x( 'Breaking News', 'Post type general name', 'textdomain' ),
        'singular_name'         => _x( 'Breaking News', 'Post type singular name', 'textdomain' ),
        'menu_name'             => _x( 'Breaking News', 'Admin Menu text', 'textdomain' ),
        'name_admin_bar'        => _x( 'Breaking News', 'Add New on Toolbar', 'textdomain' ),
        'add_new'               => __( 'Add New', 'textdomain' ),
        'add_new_item'          => __( 'Add New Breaking News', 'textdomain' ),
        'new_item'              => __( 'New Breaking News', 'textdomain' ),
        'edit_item'             => __( 'Edit Breaking News', 'textdomain' ),
        'view_item'             => __( 'View Breaking News', 'textdomain' ),
        'all_items'             => __( 'All Breaking News', 'textdomain' ),
        'search_items'          => __( 'Search Breaking News', 'textdomain' ),
        'parent_item_colon'     => __( 'Parent Breaking News:', 'textdomain' ),
        'not_found'             => __( 'No breaking news found.', 'textdomain' ),
        'not_found_in_trash'    => __( 'No breaking news found in Trash.', 'textdomain' ),
        'featured_image'        => _x( 'Breaking New Cover Image', 'Overrides the “Featured Image” phrase for this post type. Added in 4.3', 'textdomain' ),
        'set_featured_image'    => _x( 'Set cover image', 'Overrides the “Set featured image” phrase for this post type. Added in 4.3', 'textdomain' ),
        'remove_featured_image' => _x( 'Remove cover image', 'Overrides the “Remove featured image” phrase for this post type. Added in 4.3', 'textdomain' ),
        'use_featured_image'    => _x( 'Use as cover image', 'Overrides the “Use as featured image” phrase for this post type. Added in 4.3', 'textdomain' ),
        'archives'              => _x( 'Breaking New archives', 'The post type archive label used in nav menus. Default “Post Archives”. Added in 4.4', 'textdomain' ),
        'insert_into_item'      => _x( 'Insert into breaking new', 'Overrides the “Insert into post”/”Insert into page” phrase (used when inserting media into a post). Added in 4.4', 'textdomain' ),
        'uploaded_to_this_item' => _x( 'Uploaded to this breaking new', 'Overrides the “Uploaded to this post”/”Uploaded to this page” phrase (used when viewing media attached to a post). Added in 4.4', 'textdomain' ),
        'filter_items_list'     => _x( 'Filter breaking news list', 'Screen reader text for the filter links heading on the post type listing screen. Default “Filter posts list”/”Filter pages list”. Added in 4.4', 'textdomain' ),
        'items_list_navigation' => _x( 'Breaking News list navigation', 'Screen reader text for the pagination heading on the post type listing screen. Default “Posts list navigation”/”Pages list navigation”. Added in 4.4', 'textdomain' ),
        'items_list'            => _x( 'Breaking News list', 'Screen reader text for the items list heading on the post type listing screen. Default “Posts list”/”Pages list”. Added in 4.4', 'textdomain' ),
    );
 
    $args = array(
        'labels'             => $labels,
        'public'             => true,
        'publicly_queryable' => true,
        'show_ui'            => true,
        'show_in_menu'       => true,
        'query_var'          => true,
        'menu_icon'          => 'dashicons-megaphone',
        'rewrite'            => array( 'slug' => 'breaking-news' ),
        'capability_type'    => 'post',
        'has_archive'        => true,
        'hierarchical'       => false,
        'menu_position'      => null,
        'supports'           => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments' ),
        'show_in_rest'       => true
    );
 
    register_post_type( 'breaking-news', $args );
}
 
add_action( 'init', 'breaking_news_cpt_init' );

Post Type Considerations

While this content could live forever as “Breaking News”, it could be that, after the news item is past “breaking” it should be turned into a different post type, or archived. This can be done by changing the post_type for the Breaking News item in the database. There are many ways to do this beyond the scope of the current example, just something to keep in mind.

Authentication and CORS Notes

The example above does not do anything for auth or CORS. You can use your discretion on how or when you’d like to use auth or CORS. The above is meant to “get you started in the right direction”, which might not include auth or CORS. These can be added later.