Skip to Main Content
Palantir.net
Menu
Close Menu

Conscious Decoupling: The Case of Palantir.net

Our new site uses VueJS to produce single page applications. By tying these applications to content creation, our content editors can create dynamic new pages without needing an engineer.

Alex Brandt recently wrote about the new redesign of the new Palantir.net site: what the goals were, what we wanted to improve, and the process by which we approached the project. I want to speak more from a development viewpoint around how we decoupled the new www.palantir.net site in order to create better relationships between content.

A major goal of our 2018 redesign was to feature more content and make it easier for people to surface topics that interest them. The strategic plan and design called for a system that allows people to filter content from the home page and other landing pages (such as our Work page).


Homepage filtering by topicIn the modern web, people expect this filtering to take place without a page refresh. Simply select the filter and the content should update immediately.

This presents an opportunity to explore methods for achieving this effect. In addition, the following features were desired:

  • The ability to feature specific content at the top of the page
  • A process to insert content other than Drupal pages into the list display
  • A way to select what types of content appear on the page
  • A method to restrict the total count of items displayed on the page
  • The ability to add one or two filters to the page; or none at all

From the developer’s point-of-view, we also added:

  • The ability to allow editors to create and configure these dynamic pages without additional programming

The design and development of the new site followed our understanding of what “content” means to different teams. We know that understanding how to implement the design requirements isn’t enough. We had to think through how editors would interact with (and control) the content.

There is a lot of talk around “decoupled” Drupal these days — the practice of using Drupal as an editing environment and then feeding data to a front-end JavaScript application for rendering. Certainly we could have chosen to decouple the entire site. That process, however, brings extra development time and overhead. And in our case, the site isn’t large enough to gain any advantage from rapidly changing the front-end.

So instead we looked at ways to produce a dynamic application within Drupal’s template system. Our technical requirements were pretty standard:

  • A template-driven JavaScript content engine
  • Rendering logic (if/else and simple math)
  • Twig compatibility
  • A single source file that can be served from CDN or an application library

This last requirement is more a personal preference, as I don’t like long, fixed dependency chains during development. I specifically wanted a file I could drop in and use as part of the Drupal front-end.

Based on these requirements and a few basic functionality tests, we settled on the VueJS library. Vue is a well-documented, robust framework that can be run server-side or client-side. It provides DOM-manipulation, templated iteration, and an event-driven interaction API. In short, it was perfect for our needs.

Even better, you can start using it immediately:

<script src="https://cdn.jsdelivr.net/npm/vue"></script> 

At the core of Vue.js is a system that enables us to declaratively render data to the DOM using straightforward template syntax. Vue uses the handlebars syntax -- familiar to Twig users -- to define and print variables set by the application:

<div id="app">
  {{ message }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

When working with Twig, which also uses handlebars to wrap variables, we must wrap VueJS variables in the {% verbatim %} directive like so:

<blockquote class="grid-text__quote">
  {% verbatim %}{{ item.slug }}{% endverbatim %}
</blockquote>

Unless you hardcode variables, Vue pulls all of its data via JSON, which can be provided out-of-the-box by Drupal’s Views module.

To make the application work, we needed to provide the following elements:

  • A JSON feed for content to display, with featured items at the top
  • A JSON feed for each filter to use
  • A JSON feed for insert content

The first elements -- content -- consists of two JSON directives controlled by editors. First, there is a Featured Content field that can be used to select content for the top of the page:

Feature Content list

Editors may choose to only populate this content, and they may choose as many items as they wish. Below this content, we optionally add additional content based on type. Editors may select what types of content to add to the page. For the homepage, we include four content types:

Content Type list

Editors may then select which filters to use, if any are desired. Two options are available, but one can be selected. The filters may be sorted as desired.

Taxonomy Lists

Further, if the editor selects the Filter by Type option, the first filter will be replaced by a Content Type list taken from the selections for the current page. This technique is made easier by the dynamic nature of the VueJS application, which is expecting dynamic content. It has the added bonus of making the work future proof, as editors can add and adjust pages without additional coding.

Case study filtering

Lastly, editors can add “insert” content to the page. These inserts are Drupal Paragraphs -- custom fielded micro-content -- that optionally appear sprinkled through the content.

Inserts

These inserts leverage Vue’s logic handling and template system. Here is a code snippet for the inserts:

  <template v-if="item.parent_id">
    <div v-if="item.type === 'insert_cta'" class="grid-item grid-item--join">
      <a class="grid-link" v-bind:href="item.link_url" v-bind:style="{ backgroundImage: 'url(\'' + background(index, item.image) + '\')' }">
        <div class="grid-link__join">
          <span class="grid-link__join-link">{% verbatim %}{{ item.link_text | decode }}{% endverbatim %}</span>
          <span class="grid-link__join-message">{% verbatim %}{{ item.slug | decode }}{% endverbatim %}</span>
        </div>
      </a>
    </div>
    <div v-else-if="item.type === 'insert_quote'" class="grid-item grid-item--quote">
      <div class="grid-text">
        <div class="grid-text__content">
          <blockquote class="grid-text__quote">
            {% verbatim %}{{ item.slug | decode }}{% endverbatim %}
          </blockquote>
          <footer>
            <cite class="grid-text__author">{% verbatim %}{{ item.author | decode }}{% endverbatim %}
              <span v-if="item.link_url">
                <a v-bind:href="item.link_url">
                {% verbatim %}{{ item.link_text | decode }}{% endverbatim %}
                </a>
              </span>
              <span v-else>
                {% verbatim %}{{ item.link_text | decode }}{% endverbatim %}
              </span>
            </cite>
          </footer>
        </div>
      </div>
    </div>
    <div v-else class="grid-item grid-item--simple">
      <div class="grid-text">
        <div class="grid-text__content">
          <div class="grid-text__simple-text">{% verbatim %}{{ item.slug | decode }}{% endverbatim %}</div>
          <a class="grid-text__simple-link" v-bind:href="item.link_url">{% verbatim %}{{ item.link_text | decode }}{% endverbatim %}</a>
        </div>
      </div>
    </div>
  </template>

The v-if directive at the start tells the application to only render the entire template if the parent_id property is present. Since that property is unique to Paragraphs, this template is skipped when rendering a blog post or case study.

In cases of content types, we have a standard output from our JSON feed, so one template covers all use-cases:

  <template v-else>
  <div :class="'grid-item' + getClass(index, item.type, item.image)">
    <a class="grid-link" v-bind:href="item.url" v-bind:title="item.title | decode" v-bind:style="{ backgroundImage: 'url(\'' + background(index, item.image) + '\')' }">
    <div class="grid-link__content">
      <span class="grid-link__label">{% verbatim %}{{ item.type }}{% endverbatim %}</span>
      <h2 class="grid-link__title">{% verbatim %}{{ item.title | decode }}{% endverbatim %}</h2>
      <p v-show="item.summary" class="grid-link__teaser">{% verbatim %}{{ item.summary | decode }}{% endverbatim %}</p>
    </div>
    <div v-if="item.dates || item.location" class="grid-link__meta">
      <span v-if="item.dates" class="grid-link__meta-author">{% verbatim %}{{ item.dates | decode }}{% endverbatim %}</span>
      <span v-if="item.location" class="grid-link__meta-location">{% verbatim %}{{ item.location | decode }}{% endverbatim %}</span>
    </div>
    <div v-if="item.author_display" class="grid-link__meta">
      <span v-if="item.author_image" class="grid-link__meta-thumb"><img v-bind:src="item.author_image|decode" v-bind:alt="item.author_display"></span>
      <span class="grid-link__meta-author">{% verbatim %}By {{ item.author_display }}{% endverbatim %}</span>
    </div>
    <div class="grid-link__view">
      <span class="grid-link__view-link">{% verbatim %}{{ getLinkText(item.type) }}{% endverbatim %}</span>
    </div>
    </a>
  </div>
  </template>

Note the v-bind directive here. Vue cannot parse variables directly in HTML tag properties, so it uses this syntax to interact with the DOM and rewrite the element.

Other nice features include adding method calls like {{ getLinkText(item.type) }} that let Vue perform complex calculations on data elements. We also use Vue’s extensible filter system {{ item.summary | decode }} to perform actions like HTML escaping.

For instance, we pass the index position, content type, and background image (if present) to the getClass() method of our application:

// Get proper class for each cell.
getClass: function(index, type, image) {
  var $class = '';
  if (index == 2 || index == 5 || (index > 12 && index % 5 == 1)) {
    $class = $class + ' grid-item--lg';
  }
  if (type == 'Case Study') {
    $class = $class + ' grid-item--cs grid-item--dark';
  }
  else if (type == 'Collection') {
    $class = $class + ' grid-item--collection';
    if (index % 2 == 1) {
      $class = $class + ' grid-item--dark';
    }
  }
  else {
   $class = $class + ' grid-item--default';
  }
  if (image === undefined || image.length === 0) {
    if (index % 2 == 1) {
      return $class + ' grid-item--dark';
    }
    return $class;
  }
  return $class + ' grid-item--dark';
},

This technique lets us provide the light and dark backgrounds that make the design pop.

Light and dark blocks

The end result is exactly what we need to deliver the experience the audience expects. And we get all that within a sustainable, extensible platform.

Is a decoupled Drupal site right for you? For us, a dynamic (yet not fully decoupled) instance made the most sense to improve the experience for our site users. We’d love to discuss whether or not the same flexibility would be beneficial for your site. Drop us a line via our contact form, or reach out via Twitter (@palantir).

Let's start a conversation. New Call-to-action