Behind the front-end: Yoast SEO for Shopify

It is April 2021 and amidst a global pandemic, we are about to embark on our greatest venture yet: developing an SEO app for Shopify. For a development department so focused on WordPress for so long, this is both a dream and a nightmare. Starting fresh on a new platform offers endless possibilities, but even more challenges along the way. Now, after months of intense development, Yoast SEO for Shopify has seen the light of day. And I feel obliged to share this technical journey with you. Not because I think it needs explaining, but because I couldn’t be more proud of the giant leap forwards we took with this project. This is a story of how we, from our attics and kitchen tables, brought ‘SEO for everyone’ to Shopify.

Well, actually… This is merely a look behind the front-end of our new Shopify plugin. My name is Nolle, I’m a front-end developer at Yoast and together with the Components squad, I’m to git blame for just that. I want to start by taking you through the main technologies driving the user interface and I want to end with why we didn’t actually build this front-end for Shopify at all. Come again?

Technologies

Our tech stack was primarily determined by what we’re familiar with and what Shopify promotes. You can experiment when you get a clean slate like this. But creating a really robust solution based on past experiences is even better. And that’s exactly what we did, though there are some firsts in the list below.

React

React obviously needs little introduction. It’s already powering most of our highly interactive interfaces in WordPress and for Shopify we took this even further. The front-end is completely written in React and consists of two single page applications. One for general SEO settings and one for optimizing content (more on this separation later). This makes for a user experience that feels much faster and is packed with cool features like optimistic UI, skeleton loaders and live form feedback.

Technically, we’ve moved away completely from class-based components. We’re now committed to a system of smart and dumb functional components. Smart (controller) components are separated from their dumb (view) counterparts so that the latter can be generalized and reused. We’ve also started to follow a ‘hooks over HOCs’ principle. A principle where we favor React hooks over higher-order components (and basically everything else) as much as possible. This all makes for a modern and future-proof React codebase.

Redux and WordPress Data

Like React, Redux is a familiar face here at Yoast. We’ve been using it to manage centralized state in our JavaScript applications for a long time. In our Shopify plugin, we’re using the WordPress Data module to work with Redux because of its familiarity and benefits. It wraps the core Redux API in a thin layer of optimizations like selector memoization and a Redux Saga like system for writing asynchronous actions, called controls.

Next to a nice and nested state structure, we feel like this store has become one of our cleanest yet by adhering to a few simple rules. For instance, we got rid of those dreadful state Booleans (isLoading, isSuccessful, etc.) and replaced them with status constants. That way, the state of something asynchronous for instance can be traced back to one place. Instead of deriving it from multiple Booleans. No more weird edge cases in-between states!

// Replace those bug prone state Booleans
// with status constants.

// Using status booleans in state.
const state = {
  isLoading: true,
  hasError: false,
};

// Using status enums in state.
const state = {
  status: "loading", // || "idle" || "success" || "error"
};

We’ve also tried to be strict in writing actions in an event-like manner. Meaning we don’t ‘set’ anything in the store, we only ‘inform’ it that something (an event) has occurred. For instance, the difference between setActiveItem and itemActivated is subtle. But the latter is not at all coupled to a specific reducer as the former indicates. In a good Redux store, every reducer is able to respond to every action if needed. Again, following simple rules like these has made all the difference for us in creating a robust store.

// Event-like Redux actions promote looser coupling
// between reducers and the actions it responds to.

// Using setter like actions.
const authenticationReducer = ( state, action ) => {
  switch ( action.type ) {
    case "setIsAuthenticated": return {
      ...state,
      isAuthenticated: action.payload.isAuthenticated,
    };

    case "setUsername": return {
      ...state,
      username: action.payload.username,
    };

    default: return state;
  }
};

const notificationReducer = ( state, action ) => {
  switch ( action.type ) {
    case "setAuthenticationNotification": return [
      ...state,
      {
        type: "success",
        message: "You were successfully authenticated!",
      },
    ];

    default: return state;
  }
};

// Using event like actions.
const authenticationReducer = ( state, action ) => {
  switch ( action.type ) {
    case "user/authenticated": return {
      ...state,
      status: "authenticated",
      username: action.payload.username,
    };

    default: return state;
  }
};

// Another reducer reacts to the same action.
const notificationReducer = ( state, action ) => {
  switch ( action.type ) {
    case "user/authenticated": return [
      ...state,
      {
        type: "success",
        message: "You were successfully authenticated!",
      },
    ];

    default: return state;
  }
};

Tailwind

With great UX comes great CSS responsibility. At Yoast, like most other companies, we witnessed first-hand that writing a scalable and maintainable CSS codebase is complicated. We’ve tried everything from plain CSS, preprocessors and CSS-in-JS options to all kinds of frameworks. But we never got a firm grasp on dealing with consistency, exceptions and learning curves. Finally, with Tailwind, we feel we’ve found a durable solution: stop writing CSS altogether.

Tailwind is a utility-first CSS framework and the concept is simple: combine single-purpose CSS classes on your elements to get the styling you need. Instead of combining CSS properties in your stylesheets to get there. Can you see how this immediately solves the scalability issue? Writing CSS becomes a matter of finding the right combinations of utilities instead of, well, writing actual CSS. Of course, Tailwind provides a solution for combining utilities into new class names for easy application, but you’re still not really writing CSS. Tailwind comes with great documentation including many examples, is easy to configure and provides great performance by purging unused styles. I recommend it to everyone.

// Combine multiple Tailwind utility classes into a custom
// .button class using the @apply directive.

.yst-section {
  @apply yst-max-w-5xl lg:yst-grid lg:yst-grid-cols-3 lg:yst-gap-8 yst-mx-8 yst-mb-8 yst-border-b yst-border-gray-200 yst-pb-8;
}

Platform agnosticism

Now let’s get back to that thing I said about this front-end not being built for Shopify at all. With this project, we set out to create a universal Yoast interface, not specifically tailored to one platform. So instead of building a user interface, we will actually be building an interface for building user interfaces. A higher-order user interface, if you will. This kind of agnostic approach offers challenges of its own, mainly those considering flexibility.

Configurability

Not all platforms offer the same editable content types. And some platforms might not even benefit or support certain SEO features at all. In other words, the front-end has to be highly configurable to fit the needs and capabilities of many platforms. Almost like an SEO settings form builder.

We solved this technically by exposing an app initializer function that accepts the configuration for the specific platform at hand. This object mainly consists of a list of content types and their supported SEO features and a list of generally supported SEO features. Based on this configuration, the initializing function creates a Redux store and other contexts, sets up routes and form components and registers callbacks for handling retrieving and saving data. It then returns an app object with a render method. Which is basically just a ReactDOM.render call with its first argument, the component, already supplied. Now the implementor can render the app in a DOM element of choosing while remaining unaware of the technology responsible for that rendering.

This approach has proven to offer the flexibility and decoupling we were looking for. Everything the user interface needs to know from the implementor is shared through a single configuration object. In theory, we could now switch out Redux for React’s useReducer. Or replace React with Vue entirely without having to touch the implementor whatsoever. Magic…

// A basic example of how to initialize
// and render the new Yoast user interface.
import initializeSettings from "@yoast/admin-ui/settings";

const { render } = initializeSettings( {
  isSetting1Enabled: true,
  isSettings2Enabled: false,
  contentTypes: [
    {
      name: "post",
      isContentTypeSetting1Enabled: false,
      ...etc,
    },
  ],
  handleSave: async ( data ) => {
    const response = await saveData( data );
    return response.status;
  },
  ...etc,
} );

// Start rendering React in the #root.
render( document.getElementById( "root" ) );

Specifics to Shopify

Of course, each platform is going to have its own little caveats and Shopify was no exception. There is a thin layer of JavaScript that sits between the UI and the API to tackle these and make sure the interface can remain agnostic. Since this is a post about our Shopify plugin, I want to quickly highlight the main responsibilities of this in-between adapter layer specific to Shopify.

Extending the Shopify editor

Firstly, Shopify does not offer a way to properly extend its content editor with a Yoast sidebar as WordPress does. Therefore, the Shopify plugin features a separate React app for optimizing your content’s SEO using our own little editor. This editor offers a clean and simple experience and supports almost all content relevant to SEO. By hooking into Shopify’s plugin navigation API, users can switch between their general SEO settings and the Optimize app. While staying comfortably within the Yoast plugin.

File uploading

File uploads proved to be a particularly challenging feature. It might sound simple, but each platform has its own way of dealing with uploads. While in WordPress we could rely on the media library, in Shopify we had to write the upload logic ourselves. Therefore, the agnostic front-end only shows a dumb file uploader that leaves its behavior on user interaction to a callback passed through the configuration object. In Shopify, this callback renders a hidden file input and immediately triggers a click on that element causing the browser to show its native file browser. On other platforms that logic might be completely different.

Blogs vs. blog posts

A quirk in Shopify we had to overcome is that the blog post content type is coupled tightly with the blog type. In WordPress terms, the blog type is a separate post type but also a mandatory taxonomy of the blog post type. The pain here being you are unable to retrieve blog posts using the Shopify API without specifying a blog identifier as well. We didn’t want this structure to break our pattern of optimizing per content type. So we decided to show each term of the blog ‘taxonomy’ (i.e. blog #1) in the main navigation. This way, you won’t be optimizing blog posts, but you will be optimizing blog posts in blog #1. Luckily our setup proved flexible enough to handle this and the user experience is much better for it.

Data transformation

Lastly, by designing the front-end’s data structure without a specific platform in mind, we were bound to have some data mismatches between API and UI. A nicely structured transformation layer to the rescue! Each callback passed through the configuration object (i.e. handleSave) is responsible for transforming the data it receives into the structure it expects.

const transformDataForApi = ( data ) => ( {
  seoTitle: data.seo.title,
  metaDescription: data.seo.description,
  ...etc,
} );

const handleSave = async ( data ) => {
  const dataForApi = transformDataForApi( data );
  const response = await saveData( dataForApi );
  return response.status;
};

Conclusion

I hope I gave you some insight into our fun and challenging journey of building the front-end for Yoast SEO for Shopify. As I said, I’m very proud of the level of abstraction we obtained in this project, paving a clear path for the future. Right now, we’re very much looking forward to receiving user feedback, so we can keep on improving their experience.

If you want to read more about why we’ve built this app and the whole process around it, our CEO Thijs de Valk wrote an interesting piece on how we scoped, built and launched our Shopify app. Thanks for reading and if you have any front-end related questions, I’d be happy to answer them in the comments. Have a great day!

Read more: We’ve officially launched Yoast SEO for Shopify »

Coming up next!


3 Responses to Behind the front-end: Yoast SEO for Shopify

  1. elineyoast
    elineyoast  • 10 months ago

    Nice read Nolle!

  2. RobertDiect2145
    RobertDiect2145  • 10 months ago

    “After I originally commented I seem to have clicked the -Notify me when new comments are added- checkbox and from now on each time a comment is added I receive 4 emails with the same comment. There has to be an easy method you can remove me from that service? Thank you!”

    • Camille Cunningham
      Camille Cunningham  • 10 months ago

      Hi there, Robert! You should be able to unsubscribe to these notification emails by clicking on the unsubscribe link in the email. Hope this helps :)