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:
- Create a Breaking News item.
- Publish it quickly with Selective Publish on Strattic (should take less than one minute).
- Continue to update the content without needing to republish the post.
- 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):
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.