Serving Dynamic Content on Cached Web Pages

Published

Problem: How to best load comments on cached Pages?

I recently added commenting to my WordPress blog. Comments are dynamic, and change more often than the blog article content itself. I needed a way to guarantee users would see the most up-to-date list of comments despite the rest of the page being cached.

Research: Alternatives for loading Dynamic Content

I researched ways to serve dynamic / user-generated content on static cached pages. Many of the options included loading content asynchronously in the browser, or using targeted cache invalidations whenever new dynamic content was created.

Ultimately the three approaches I considered were:

  1. Output dynamic content with the web page (default) – content is cached with the web page, and the page cache is invalidated whenever new dynamic content is created.
  2. Load dynamic content over AJAX – load content after the page has loaded from a REST API that does not have caching enabled.
  3. Combination – output content with the initial page, but perform a check after page load to confirm dynamic content is up to date. Force a JavaScript/AJAX update if content is stale.

I favored the third option because it provided a way to cache the dynamic content (improving the performance and scalability of the site), displayed it immediately on page load (possible UX and SEO implications), while still ensuring the content was current. In addition to being the most robust, this option also required the most effort to implement..

Implementation: Loading WordPress Comments over AJAX

I ended up choosing the second option: loading the comments over AJAX. I custom build this functionally into my WordPress site rather than using plugin (even through there were many available). I had to create the following components:

  1. Comments Template (PHP) – generates the HTML for the comments container and the comment submission form.
  2. Content Submission Form (HTML/JavaScript) – an HTML form that POSTs user comments to the WordPress Comments REST API (see the Data API section below for details).
  3. Rendered Comment List (JavaScript) – logic executed after page load to retrieve the list of user comments as JSON and render them into the DOM using the Mustache JS library.

Backend Adjustments

I made a few adjustments to my theme’s functions.php file to accommodate this solution.

First I needed to enable anonymous commenting through the REST API:

// allow anonymous access to WordPress v2 REST API
function filter_rest_allow_anonymous_comments() {
  return true;
}
add_filter('rest_allow_anonymous_comments','filter_rest_allow_anonymous_comments');

Next I added a check for a honeypot field present on the comment submission form to help prevent SPAM. This was done by confirming the “honeytcpot” POST parameter (corresponding to the name of the honeypot input element) was left empty:

// filter to check for honeypot - allow select html tags
function filter_rest_pre_insert_comment($prepared_comment) {
  if(isset($_POST['honeytcpot']) && !empty($_POST['honeytcpot'])) {
    return new WP_Error( 'invalid_comment', 'honeypot field populated - denying request', array( 'status' => 403 ) );
  }
  // return comment (untouched)
  return $prepared_comment;
}
add_filter('rest_pre_insert_comment', 'filter_rest_pre_insert_comment');

The WordPress hook I used here is a great place for any custom pre-processing that may need to be done on user submitted comments.

Email Notifications

I configured WordPress to send email notifications whenever new comments were made. Following this guide allowed me use the Gmail SMTP Server when sending emails from WordPress.

It turns out the WordPress does not send notifications if comments are created through the REST API. I found a solution for this issue here, which involved adding one more code block to my functions.php file:

// enable new comment notification for comments created via REST
function comment_inserted_notification($comment_id, $comment_object) {
  wp_notify_postauthor( $comment_id );
}
add_action('wp_insert_comment','comment_inserted_notification',99,2);

Data API

Luckily the WordPress REST API supports both retrieving a list of comments (via a GET request) and creating comments (via a POST request). Here are example cURL requests:

# list comments for a given article (GET request)
curl -X GET 'https://taylor.callsen.me/wp-json/wp/v2/comments?per_page=100&post=426'

# create a new comment (POST request)
#  POST body parameters: author_name author_email author_url content post parent
curl -X POST \
  https://taylor.callsen.me/wp-json/wp/v2/comments \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'author_email=taylor.callsen%40gmail.com&author_name=Taylor%20Callsen&content=Here%20is%20an%20example%20comment!&post=426&author_url=https%3A%2F%2Ftaylor.callsen.me&parent=0&'

Caching Layer Pass-through

I configured the caching layer (AWS CloudFront in my case) to pass-through all requests made to the WordPress Comments REST API without caching. Luckily this was easy to do by creating a new Behavior inside of my existing CloudFront distribution:

I adjusted the “Allowed HTTP Methods” to include POST requests to support the new comments submission form.

I also adjusted the TTL settings to 0, which essentially told CloudFront not to cache any of the requests handled by this behavior (more information on setting TTLs is available here).

SEO Considerations

There has been a lot of discussion over the years about if Google does truly index content loaded over AJAX. While many people say yes, and specifications for doing so have been created (and deprecated), it is probably safest to serve all content that is relevant from an SEO perspective along with the page.

While this solution works well for me now, I do hope to eventually adjust the comments to render with the page initially, and perform a JavaScript/AJAX confirmation after page load to ensure the comment list is current (option 3 above).

Comments

Loading comments..

No responses yet

Leave a Reply

Your email address will not be published. Required fields are marked *