New Website: Drupal 11, Astro & Remark42

With the (long awaited) retirement of Drupal 7, I've re-built my site using Drupal 11, Astro, and Remark 42.

Drupal 7 has had a great run - but with its retirement in December, it's finally time for a re-build after over 13 years. I always like to find a way to do or learn something new when working on a personal project, and this new site is no different.

Tech stack:

Infrastructure:

  • Digital Ocean w/ Docker Compose for Drupal 11 & Remark42
  • Netlify with Cloudflare in front of it for Astro
  • CircleCI for Drupal 11 deployments
  • BitBucket Pipelines for Astro deployments

Technology principals

In determining what tech stack I wanted to consider a few different principals.

Firstly it was important to me to use Open Source, tools. Not only because I believe in the Open Web, but it was also likely to be the best set of tools to create a site that was performant, without unnecessary tracking scripts from 3rd parties.

Secondly I wanted to explore more in the Jamstack and Composable Architecture space. I've overseen the delivery of Jamstack and Composable solutions before, but this was a great chance to get hands-on.

Finally I'm also a fan of Static Site Generation (SSG), and a personal blog that changes infrequently was a great place to use it. Static sites are typically going to be more performant, and also give a lot of options when it comes to hosting at a reasonable price.

Drupal 11, Astro and Layout Builder

I wanted to learn how to use Drupal as a headless system, whilst also making use of its Layout Builder functionality. I had to patch Drupal core to expose LayoutBuilder to the JSON API, but that was fairly straight forward.

The main challenge was setting up a way to account for the different kinds of blocks that will be exposed by Drupal. My blocks are fairly straight forward: Text, Code, Image, but with a few different display options like width of image, border display etc.

In the end I created an Astro component that just switched the block to use the correct template based on the block type.

{
  renderedBlocks.map(({ region, blockData }) => {
    let content = "";

    // Switch statement to choose the right component based on block type
    switch (blockData.type) {
      case "block_content--basic":
        content = <BlockBasic {...blockData} />;
        break;
      case "block_content--image":
        content = <BlockImage {...blockData} />;
        break;
      case "block_content--code":
        content = <BlockCode {...blockData} />;
        break;
      default:
        content = "<p>Unknown block type</p>";
        break;
    }

    return <div class="pb-5">{content}</div>;
  })
}

Content migration

My previous Drupal 7 blog pages were effectively big WYSIWYG fields (yeah, I know), so given the relatively small number of pages (17 blog pages) - and the change from WYSIWYG to Layout Builder and Blocks - it didn't make sense to use a migrate script. In the end I just copied things across manually, breaking the content out in to the appropriate block types - and it didn't take too long to do.

Comment migration from Drupal 11 to Remark 42

I've collected quite a lot of comments over the years, especially on a couple of my more popular posts - and many of them have really valuable hints and tips that built on the content on of posts themselves. So for that reason I wanted to migrate comments from Drupal 7, to a format that could be imported into Remark42. 

I built a Drupal 7 module that created a .json file that could be imported into Remark 42.

<?php

/**
 * @file
 * Exports Drupal 7 comments to Remark42 JSON format (Commento format).
 */

/**
 * Implements hook_menu().
 */
function remark42_export_menu() {
  $items['admin/config/content/remark42_export'] = array(
    'title' => 'Remark42 Export (Commento)',
    'description' => 'Export Drupal 7 comments to Remark42 JSON (Commento format).',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('remark42_export_page'),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
 * Form definition for admin/config/content/remark42_export.
 */
function remark42_export_page($form, &$form_state) {
  $form['remark42_site_id'] = array(
    '#type' => 'textfield',
    '#title' => t('Remark42 Site ID'),
    '#description' => t('Enter your Remark42 site ID.'),
    '#required' => TRUE,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Export Comments (Commento)'),
  );
  return $form;
}

/**
 * Submit handler for the export form.
 */
function remark42_export_page_submit($form, &$form_state) {
  $site_id = $form_state['values']['remark42_site_id'];

  try {
    $query = db_select('comment', 'c');
    $query->fields('c', array('cid', 'nid', 'uid', 'name', 'mail', 'created', 'pid'));
    $query->fields('f', array('comment_body_value'));
    $query->fields('ua', array('alias'));
    $query->leftJoin('field_data_comment_body', 'f', 'c.cid = f.entity_id AND f.entity_type = :comment_type', array(':comment_type' => 'comment'));
    $query->leftJoin('url_alias', 'ua', 'CONCAT(\'node/\', c.nid) = ua.source');
    $query->condition('c.status', 1);

    $comments = $query->execute()->fetchAll(PDO::FETCH_ASSOC);

    $remark42_data = array(
      'version' => 1,
      'comments' => array(),
      'commenters' => array(),
    );

    $commenter_hexes = array();

    foreach ($comments as $comment) {
      $comment_hex = md5($comment['cid']);
      $parent_hex = $comment['pid'] ? md5($comment['pid']) : 'root';
      $url = $comment['alias'] ? ltrim($comment['alias'], '/') : "node/" . $comment['nid'];
      $domain = $_SERVER['HTTP_HOST'];

      if ($comment['uid'] == 0) {
        if ($comment['mail']) {
          $commenter_hex = md5($comment['mail']);
        } elseif ($comment['name']) {
          $commenter_hex = md5($comment['name']);
        } else {
          $commenter_hex = md5('anonymous_' . $comment['cid']);
        }
      } else {
        $commenter_hex = md5($comment['uid']);
      }

      // Add trailing slash to URL
      $url .= (substr($url, -1) !== '/') ? '/' : '';

      $remark42_data['comments'][] = array(
        'commentHex' => $comment_hex,
        'domain' => $domain,
        'url' => $url,
        'commenterHex' => $commenter_hex,
        'markdown' => str_replace(array("\r\n", "\r", "\n"), '<br>', $comment['comment_body_value']),
        'html' => '',
        'parentHex' => $parent_hex,
        'score' => 0,
        'state' => 'approved',
        'creationDate' => date('Y-m-d\TH:i:s\Z', $comment['created']),
        'direction' => 0,
        'deleted' => false,
      );

      if (!isset($commenter_hexes[$commenter_hex])) {
        $remark42_data['commenters'][] = array(
          'commenterHex' => $commenter_hex,
          'email' => $comment['mail'],
          'name' => $comment['name'],
          'link' => '', // Add user profile link if available
          'photo' => '', // Add user photo URL if available
          'provider' => 'drupal',
          'joinDate' => date('Y-m-d\TH:i:s\Z', $comment['created']), // Add user registration date if available
          'isModerator' => false, // Adjust based on user role
        );
        $commenter_hexes[$commenter_hex] = true;
      }
    }

    // Generate JSON file.
    $filename = 'remark42_comments_commento_' . date('YmdHis') . '.json';
    $filepath = file_directory_temp() . '/' . $filename;
    file_put_contents($filepath, json_encode($remark42_data, JSON_PRETTY_PRINT));

    // Offer download.
    drupal_add_http_header('Content-Type', 'application/json');
    drupal_add_http_header('Content-Disposition', 'attachment; filename="' . $filename . '"');
    drupal_add_http_header('Content-Length', filesize($filepath));
    readfile($filepath);
    unlink($filepath); // Clean up temp file.
    drupal_exit();

  } catch (Exception $e) {
    drupal_set_message(t('An error occurred during export: @error', array('@error' => $e->getMessage())), 'error');
  }
}

This allowed me to export all of my existing comments, and migrate them into Remark42, retaining their original hierarchy.

Conclusion

By building my new site headless, and by using Remark42 for comments, I've ended up with a very fast website, which isn't dependent on Drupal. Should the need occur in the future to switch to a different CMS or Comment engine, then they should be fairly simple to switch out (in theory).

I'm also able to keep the Drupal itself installation behind basic auth, reducing the likelihood of it being exposed to any security threats.

Comments