Modify article body for one particular article to add data
I ended up modifying the node when cron runs.

define("JMT_CLASSES_CLASSES_ED_NODE_ID", 574);
// 60 * 60 * 8 - measured in seconds.
define("JMT_CLASSES_EIGHT_HOURS", 28800);

/**
 * Implements hook_cron().
 */
function jmt_classes_cron() {
  // Check current config for time, if greater than 8hrs check for updates.
  $timeOfLastUpdate = \Drupal::configFactory()
    -&gt;get('jmt_classes.settings')
    -&gt;get('last_run');
  $currentTime = \Drupal::time()
    -&gt;getCurrentTime();
  $intervalSinceLastUpdate = $currentTime - $timeOfLastUpdate;

  if ($intervalSinceLastUpdate &gt;= JMT_CLASSES_EIGHT_HOURS || $timeOfLastUpdate == NULL) {
    // Check for updates.
    $raw = jmt_classes_get_classes('https://example.com/Client.WebApi/api/Route?url=query/get?url=jmt');
    if ($raw) {
      $newBodyValueHtml = jmt_classes_render_html($raw);
      $classesEdNode = Node::load(JMT_CLASSES_CLASSES_ED_NODE_ID);
      jmt_classes_update_class_node($classesEdNode, $newBodyValueHtml);

      \Drupal::configFactory()-&gt;getEditable('jmt_classes.settings')
        -&gt;set('last_run', $currentTime)
        -&gt;save();
    }
  }
}

/**
 * Update node with new html for the body field and log that it was updated.
 *
 * @param string $node
 *   The node (output page) that should be updated with new data.
 * @param string $newBodyValueHtml
 *   The HTML to set the body to.
 */
function jmt_classes_update_class_node($node, $newBodyValueHtml) {
  $node-&gt;body-&gt;value = $newBodyValueHtml;
  $node-&gt;save();
  \Drupal::logger('jmt_classes')-&gt;notice("Successfully updated body field of articles/upcoming-education-classes-field-days");
}

/**
 * Get raw json output for JMT classes. Log error or success.
 *
 * @param string $uri
 *   JMT api uri: should be something like:
 *   'https://example.com/Client.WebApi/api/Route?url=query/get?url=jmt'.
 *
 * @return string
 *   Raw json data or empty if nothing returned from request.
 *
 * @throws \GuzzleHttp\Exception\GuzzleException
 */
function jmt_classes_get_classes($uri) {

  try {
    $response = \Drupal::httpClient()-&gt;request('GET', $uri, ['timeout' =&gt; 120]);
  }
  catch (Exception $e) {
    \Drupal::logger('jmt_classes')-&gt;error("Attempted to get data 
from $uri but failed. $php_errormsg");
    return '';
  }

  $data = (string) $response-&gt;getBody();
  $data_array = json_decode($data);

  // If this isn't json data return nothing.
  if (empty($data_array)) {
    return '';
  }
  // Otherwise, return raw json data.
  else {
    \Drupal::logger('jmt_classes')-&gt;notice("Successfully got data from $uri");
    return $data;
  }
}


This has the advantage that I don't do any preprocessing. The change happens when the data changes when cron runs, not on every page load. This is important because fetching the data is SLOW!