Cover
Project

Let me introduce and walk you through Baretheme

Introduction
Let me introduce and walk you through Baretheme

This was just another waste of time, like always. I start projects with super high expectations and I am super excited and motivated. Baretheme kept me motivated for months. And now, again, I am not sure where all this is heading. Let's quickly recap what I had in mind, what I actually solved and where problems arose.

I am a huge fan of LDD - Learning Driven Design (I don't even know if that term exists?). My main goal of starting new projects is not to make money, it's to learn new things. That's why I always allow myself to use every new technology I find on the web and even to rewrite the whole project if I think another technology is better suited. Something I cannot do on my full time employment as a frontend developer, that would be horrible for the projects budget. And that's why I wanted to start writing about my experience.

So what's Baretheme?

Baretheme  is my approach on creating a theme for a headless CMS. At this time it's based on DatoCMS. No it was more than just an approach, I'd say it's 90% finished. You can even visit it on baretheme.com. It's actually deployed! Yay.

But have you ever heard of the 90/90 rule? It says that, when you think you have already done 90% of the project, you are only half way there. And that's the case with this project at the moment. Now that I am almost done I am not sure if I headed in the right direction and if it's worth the time to finish the rest.

What was the idea?

The idea was generally just to create a theme for a headless CMS, as I said. Basically just because I love the idea behind decoupling the data from the frontend. This is the future for sure.

The core feature of Baretheme is its modularity. Everything is built with components be it the front page or a blog page. Every page can be architected using a set of components, there is no template for blog posts or standard pages. You can use any component on any page.

The data is based on the concept of Documents and Collections. For example the home page is just a document without a special slug. A blog is just a collection of articles, an article is just a document. Each document has some meta fields and components at its heart. You get the idea. The CMS must be able to add components in any order and even to nest those any levels deep. This is where DatoCMS falls short, because their Modular Content field does not allow nesting.

Server Side Rendering

Baretheme was meant to be used as a static website so SSR was a must. This was the first reason for a complete rewrite of Baretheme as I originally started Baretheme with Vue and Nuxt, because those are also used in my company. While I really loved Nuxt in the first place I really started to hate it so much that I decided to switch vom Vue to React. Nuxt was nice as long as I used it with it's basic feature set, but as soon as I needed to implement my own modules and modules that depend on other modules it was a nightmare, debugging was a nightmare. I was so depressed with that development experience that I have completely thrown away my whole project and started from scratch. Why? Because Nuxt is the best way to go for server side rendering when you use Vue.

I was doing React development already two years ago and remembered there was something called Gatsby and I had a deeper look into it again. I found out it really evolved into probably the biggest framework in the React world when it comes to static site generation, exactly what I was looking for. I can tell you, I never looked back. Gatsby is just awesome.

The monorepo setup

As I already knew I wanted to be able to install specific components on demand, I thought it would be best if you could just install those via NPM. So I had to setup Baretheme as a monorepo. I needed to maintain the version numbers and dependencies of multiple packages and even release all at once. Here comes Lerna into play. But as I first used Lerna in conjunction with NPM I had some problems with the resolution of my dependencies that I can't remember in detail anymore, but I solved those using yarn workspaces. Lerna and yarn workspaces work together like nothing else, it's just a match made in heaven. Using yarn workspaces you can super easily access all the other packages of your project as like they would be available as node modules. I've ended up with a structure like this

package.json
lerna.json
/examples
  example-1
/packages
  addon-1
  addon-2
  addon-3
  ui
  theme

Implementing my own addon system

Not every website needs the same components. That's why I decided to implement every component as some kind of addon for Baretheme. So I had to create some kind of addon system. Each addon for Baretheme is basically also a Gatsby plugin, Baretheme is iterating through them and registers them to Gatsby.

module.exports = (themeOptions) => {
  const options = { ...config, ...themeOptions };
  const { plugins, ...siteMetadata } = options;
  let addonPlugins = [];

  plugins.forEach((plgn) => {
    let plugin;
    if (typeof plgn === 'object') {
      // eslint-disable-next-line
      plugin = require(plgn.resolve);
      if (typeof plugin === 'function') {
        plugin = plugin(plgn.options, options);
      }
    } else {
      // eslint-disable-next-line
      plugin = require(plgn);
    }

    if (plugin.plugins) {
      addonPlugins = [...addonPlugins, ...plugin.plugins];
    }
  });

  return {
    siteMetadata,
    plugins: [
      ...addonPlugins,
      'gatsby-plugin-emotion',
      //...
    ]
  }
});
gatsby-config.js

Additionally every addon exposes two files

  • client.js => required on the client side, e.g. to register components
  • index.js => required on server side, e.g. to register graphql queries

For example the teaser addon contains a client.js with these contents

import Teaser from './components/teaser';

export default {
  components: [
    {
      name: 'DatoCmsTeaser',
      component: Teaser,
    },
  ],
};

and a index.js with

const models = require('./data/models.json');

module.exports = {
  models,
  register: [
    {
      locations: ['blocks', 'before', 'after'],
      apiKeys: ['teaser'],
    },
  ],
  query: `
    ...on DatoCmsTeaser {
      id
      title
      links {
        id
      }
    }
  `,
};

wich is then loaded by the migrate script of Baretheme to include the GraphQL query and add the model to the specific locations in DatoCMS.

Creating a new toolset for DatoCMS

DatoCMS lacked some features for exporting and importing data. In order to make Baretheme addons work, I needed some way to define models for DatoCMS within my addons. For this I had to write scripts that make use of the DatoCMS Content Management API for things like importing and exporting models. As a result I've created a new NPM package containing those scripts. Also I've actually written a whole article on this over at Medium.

Duplicating DatoCMS projects
Hey guys! In order to make Baretheme maintainable I needed to be able to create new projects from a set of fields. When I started I was defining the models and fields I needed as plain JSON data…
@mmintel/datocms-tools
Useful tools for DatoCMS

CSS in JS

I love the idea of CSS in JS, especially because you can directly react to component props and write them right into your styles, instead of toggling classes on and off. I first started using Styled Components just to rewrite this later to use Emotion. Why? Because Styled Components really didn't work nicely when you want to inherit styles via the as prop. You can even use Emotion with almost the exact same syntax as Styled Components, so switching between them is not that much of a problem.

Configuring themes

Baretheme is based on two themes: light and dark. You can switch between those any time. I've implemented this using a mix of a UIProvider and a ThemeProvider. The UIProvider controls which theme is currently active, considering the darkmode settings of the system.

import store from 'store';
import themes from '@baretheme/ui/themes';

const supportsDarkMode = () => window.matchMedia('(prefers-color-scheme: dark)').matches === true;

//...

class UIProvider extends React.Component {
  //...
  
  getTheme = () => {
    if (!this.props.config.useThemeToggle) {
      return this.setState({ currentTheme: this.props.config.defaultTheme });
    }

    const lsTheme = store.get('theme');
    if (lsTheme) {
      this.setState({ currentTheme: lsTheme });
    } else if (supportsDarkMode()) {
      this.setState({ currentTheme: 'dark' });
    }
  };

  setTheme = (theme) => {
    store.set('theme', theme);
    this.setState({
      currentTheme: theme,
    });
  };
  
  toggleTheme = () => {
    if (this.state.currentTheme === 'dark') {
      this.setTheme('light');
    } else {
      this.setTheme('dark');
    }
  };
  
  //...
  
  render() {
    const { config, children } = this.props;
    const theme = themes.find((theme) => theme.name === this.state.currentTheme);
    return (
      <UIContext.Provider
        value={{
          ...this.state,
          theme,
          //...
        }}
      >
        {children}
      </UIContext.Provider>
    );
  }
}
ui.js

That way it's possible to use the methods setTheme and toggleTheme wherever needed. The current theme is then passed as context.

    <UIProvider config={{ ...config, ...themeOptions }}>
      <UIContext.Consumer>
        {(ctx) => (
          <ThemeProvider theme={ctx.theme}>
            {element}
          </ThemeProvider>
        )}
      </UIContext.Consumer>
    </UIProvider>
gatsby-browser.js + gatsby-ssr.js

Each theme is basically just a plain object containing definitions for all the colors.

Dealing with spacings

Spacings and especially margins are always pain to deal with. I follow the strict rule to never include outer spacing in my components. Why? Because you never know where a component is used. That spacing is always depending on the preceding and following component. That's why I implemented a Higher Order Component that takes care of it. It's usable like <Component mt={2} mr={1} pb={2} /> which adds a margin top, margin right and padding bottom. The calculation is based on modular scale.

Storybook

I was always in love with Storybook, even when it was still in beta. It was clear to me I will document my components with it. I have to say it's a lot of work to always write a story for all your components but it's worth it. It really starts paying off when you found a bug, you can just open a component in Storybook and play with its props.

import React from 'react';
import { action } from '@storybook/addon-actions';
import { boolean, select } from '@storybook/addon-knobs';
import Offscreen from '../components/offscreen';

export default {
  title: 'Components/Offscreen',
};

export const simple = () => (
  <Offscreen
    onResize={action('onResize')}
    onClose={action('onClose')}
    isOpen={boolean('isOpen', true)}
    position={select('position', ['top', 'right'], 'right')}
  >
    Content
  </Offscreen>
);
Story for Offscreen component

Jest + @testing-library =  ♥️

While Jest was not new to me I've discovered @testing-library in this project. And I really love it. It follows the principles of behaviour driven testing instead of implementation driven testing. It basically means that you should test how a user would use your website instead of testing technical implementation. You don't test changes in state or props, you test what happens when you click something. Here is an example

it('fires the onToggle method on click', () => {
    const handleToggle = jest.fn();
    const { container } = render(<Burger onToggle={handleToggle} />);

    fireEvent.click(container.firstChild);
    expect(handleToggle).toHaveBeenCalled();
  });

It's really that easy, easy to write and easy to read. But let's not go too much into detail on this article, I will write another article on testing with @testing-library and React soon.

Conventional commits + Husky

Another great thing that was new to me are Conventional Commits in combination with Husky. Conventional Commits are a collection of guidelines that describe how you should write your Git commits and Husky is able to add some pre-commit hooks that check if you sticked to those rules before you save a new commit. That's really nice, I love tools that make sure everybody who works on a project sticks to the same rules.

What's next?

I am currently researching possible solutions to my CMS dilemma. I like DatoCMS a lot and I have worked together with them for some of my problems, but as long as they don't allow nested elements for their Modular Content field, this system does not go hand in hand with Baretheme. I am currently trying out TinaCMS and will report my progress on this soon.

It's open source

If you want to see the code, because you are interested of want to make use of some parts, I've published it on my Github account.

mmintel/baretheme-gatsby-datocms-approach
This is an outdated approach to use Baretheme together with Gatsby and DatoCMS. - mmintel/baretheme-gatsby-datocms-approach
Marc Mintel
Author

Marc Mintel

Marc Mintel is a self taught JavaScript and Frontend Developer with heavy focus on React and Vue.

View Comments
Next Post

Seven examples to test your React components with Jest and @testing-library

Previous Post

Stop wasting money on Instagram feed planning services

Success! Your membership now is active.