Tech Policy Hub: Kentico to Drupal 8

A global tech company ran a site connecting academic researchers with the policy world: scholar profiles, their articles and quotes, blog posts, press releases and events, all organized by topic. It lived in Kentico and was moving to Drupal 8. A teammate handled the data migration. My job was everything after the content landed: the theme, the custom modules, and the Drupal configuration that turned a pile of migrated nodes into an actual site. It went on hold before launch and, as far as I know, never came back. The techniques did, though.

This project went on hold before launch and never went live, so there’s no site or archive to link to.

What I built

No article twice

The homepage was mostly blocks that each showed the latest few items of some content type. Which meant the newest article could easily show up in three places on one page. Each view got a contextual filter that excludes specific nodes, and a bit of JavaScript collected the IDs of everything already on the page, then reloaded the views over AJAX with those IDs excluded. Every block still shows fresh content, just never the same content.

  • Views
  • JavaScript
  • Drupal

Views that know where they are

Everything on the site hung off topics, so I wrote a small module that lets Views figure out its surroundings. On a topic page, it passes that topic into each view’s contextual filters, retitles the view to match (using the short name, like “AI & ML,” where one exists), and adds a “view all” link that carries the topic along. One set of views serves every topic page.

  • Views
  • PHP
  • Drupal

Blocks editors can actually use

A set of custom Layout Builder blocks gave editors the site’s building pieces. The star is the “latest” block: it shows the newest item of a type (or, for events, the next one coming up), and an editor can pin a specific item instead. If nothing’s pinned, an entity query fills the spot automatically. There was also a scholar Twitter embed that finds the handle from the scholar’s own profile, a repeatable logo strip, and a section header in the site’s signature bracket style.

  • Layout Builder
  • PHP
  • Drupal

Leaving a map

When the project went on hold, I wrote a handoff doc for whoever picked it back up: every helper function and block, the order the migrations had to run in (scholars first, so articles can find their authors), the drush commands for importing, rolling back and unsticking a stuck migration, and a heads-up that every rerun was pushing node IDs into the hundreds of thousands. That doc is the reason this case study exists.

  • Documentation
  • Drupal

Only the CSS each page needs

Instead of one giant stylesheet, every content type got its own small Drupal library. Webpack builds a separate CSS (and JS, where needed) file for each one, and the theme attaches a library only when that content type is on the page. A page showing an academic article never downloads the events or blog styles. Drupal won’t attach the same library twice, so a blog teaser in a sidebar and a full blog post can both ask for it safely.

The trick that keeps it consistent is a _global.scss made entirely of variables, placeholders and mixins. On its own it compiles to zero bytes of CSS, so every library can import it for free and still share the same colors, spacing and patterns. Bootstrap got a library of its own, which settled a tricky balancing act between its variables and ours.

Only the CSS each page needs A shared _global.scss file of variables, placeholders and mixins compiles to zero bytes on its own. Every content type imports it and gets its own small library: article, scholar, event and blog, plus a separate Bootstrap library. An academic article page attaches only the article and Bootstrap libraries; the event and blog styles are never downloaded. Webpack: one library per type _global.scss variables · placeholders · mixins 0 bytes on its own article.css scholar.css event.css blog.css bootstrap.css An article page ✓ article.css ✓ bootstrap.css ✗ event.css ✗ blog.css
A simplified example. Every library shares one set of variables and mixins, and each page attaches only the libraries for the content types it shows.
PHP
// webpack.config.js: one entry per content type
// entry: {
//   'blog-posts': ['./src/js/blog-posts.js', './src/scss/blog-posts.scss'],
// }

function sd_preprocess_node(&$vars) {
  $node = &$vars['elements']['#node'];
  switch ($node->bundle()) {
    case 'homepage':
      $vars['#attached']['library'][] = 'sd/homepage';
      break;
    case 'event':
      $vars['#attached']['library'][] = 'sd/events';
      break;
    case 'blog_post':
      $vars['#attached']['library'][] = 'sd/blog-posts';
      break;
  }
}
Each content type carries its own styles wherever it appears. Pages only pay for what’s on them.

Looking back

I never got to A/B test the per-content-type libraries before the project paused, so I can’t hand you a before-and-after number. What I can say is that the idea stuck. Years later, hiapti.com loads its scripts and styles per block and per page, and that’s part of how it scores so well. Not every project needs to launch to be worth doing.

Stack & skills

  • Drupal
  • Layout Builder
  • Views
  • PHP
  • Twig
  • Webpack
  • Sass
  • Bootstrap
  • DDEV
  • Drush
  • JavaScript
  • Performance Metrics
  • Documentation
  • MySQL