Frontity’s Mars Theme, Inside and Out

Frontity’s Mars Theme is the default starting point for any headless WordPress project built with the framework. It’s the thematic equivalent of WordPress’s own Twenty Twenty-One: a clean base that shows you how the pieces fit together before you start building your own layout. In this follow-up to our earlier guide on creating a headless WordPress site, we’re going to dig into how Mars Theme actually works, look at its core components, and walk through practical customization techniques — from renaming the package to fetching navigation menus from the WordPress REST API.

The Core Building Blocks

Before touching theme code, it helps to understand what Frontity itself is made of. The package.json file in a new Frontity project lists the core packages that make the system run:

  • frontity: the main package containing all development methods and the CLI.
  • @frontity/core: the engine responsible for bundling, rendering, merging, transpiling and serving the app.
  • @frontity/wp-source: connects to the WordPress REST API and fetches the data the theme needs.
  • @frontity/tiny-router: manages routing through window.history.
  • @frontity/html2react: converts HTML to React, using processors that match parts of the HTML and replace them with components.

On top of those, the building blocks of any Frontity project are the roots, fills, state, actions and libraries that each package exports. When a Frontity project starts, all the packages listed in frontity.settings.js are imported, and their settings and exports are merged into a single store by @frontity/core. Developer access to this store runs through @frontity/connect.

Theme Anatomy: The Key Components

Mars Theme has three important component files: /src/index.js, src/list/index.js and src/components/index.js. Each one exports components that are connected to the global state, actions and libraries.

The Theme Root

The src/index.js file, known as the Root, is the entry point that targets <div id="root"> in the page markup. It injects the roots of all installed packages required to run the project. Frontity uses extensibility patterns called Slot and Fill so themes can extend each other’s structure.

// mars-theme/src/components/index.js
import Theme from "./components";
// import processor libraries
import image from "@frontity/html2react/processors/image";
import iframe from "@frontity/html2react/processors/iframe";
import link from "@frontity/html2react/processors/link";

const marsTheme = {
  // The name of the extension
  name: "@frontity/mars-theme",
  // The React components that will be rendered
  roots: {
    /** In Frontity, any package can add React components to the site.
      * We use roots for that, scoped to the `theme` namespace. */
    theme: Theme,
  },
  state: {
    /** State is where the packages store their default settings and other
      * relevant state. It is scoped to the `theme` namespace. */
    theme: {
      autoPrefetch: "in-view",
      menu: [],
      isMobileMenuOpen: false,
      featured: {
        showOnList: false,
        showOnPost: false,
      },
    },
  },

  /** Actions are functions that modify the state or deal with other parts of
    * Frontity-like libraries. */
  actions: {
    theme: {
      toggleMobileMenu: ({ state }) => {
        state.theme.isMobileMenuOpen = !state.theme.isMobileMenuOpen;
      },
      closeMobileMenu: ({ state }) => {
        state.theme.isMobileMenuOpen = false;
      },
    },
  },
  /** The libraries that the extension needs to create in order to work */
  libraries: {
    html2react: {
      /** Add a processor to `html2react` so it processes the `<img>` tags
        * and internal link inside the content HTML.
        * You can add your own processors too. */
      processors: [image, iframe, link],
    },
  },
};

export default marsTheme;

The Theme component is the main root-level component, exported from the Theme namespace. It’s wrapped in connect(), which passes the state, actions and libraries props from the Root instance into the component.

// mars-theme/src/components/index.js
import React from "react"
// Modules from @emotion/core, @emotion/styled, css, @frontity/connect, react-helmet
import { Global, css, connect, styled, Head } from "frontity";
import Switch from "@frontity/components/switch";
import Header from "./header";
import List from "./list";
import Post from "./post";
import Loading from "./loading";
import Title from "./title";
import PageError from "./page-error";

/** Theme is the root React component of our theme. The one we will export
 * in roots. */
const Theme = ({ state }) => {
  // Get information about the current URL.
  const data = state.source.get(state.router.link);

  return (
    <>
      {/* Add some metatags to the <head> of the HTML with react-helmet */}
      <Title />
      <Head>
        <meta name="description" content={state.frontity.description} />
        <html lang="en" />
      </Head>

      {/* Add some global styles for the whole site, like body or a's. 
      Not classes here because we use CSS-in-JS. Only global HTML tags. */}
      <Global styles={globalStyles} />

      {/* Render Header component. Add the header of the site. */}
      <HeadContainer>
        <Header />
      </HeadContainer>

      {/* Add the main section. It renders a different component depending
      on the type of URL we are in. */}
      <Main>
        <Switch>
          <Loading when={data.isFetching} />
          <List when={data.isArchive} />
          <Post when={data.isPostType} />
          <PageError when={data.isError} />
        </Switch>
      </Main>
    </>
  );
};

export default connect(Theme);

{/* define Global styles and styled components used Theme component here */}
const globalStyles = css`
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
      "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  }
  a,
  a:visited {
    color: inherit;
    text-decoration: none;
  }
`;
const HeadContainer = styled.div`
  // ...
`;

const Main = styled.div`
  // ...
`;

In that example, state.source.get() retrieves the data to be rendered for the current URL path, and the component conditionally renders List, Post and other components based on what the data contains.

The List Component

The List component renders archive pages of posts. It’s exported from src/components/list/index.js using loadable from Loadable Components, so the code is split into separate bundles and only loads when a user actually visits a list view — not when they land on a single post.

// src/components/list/index.js
import { loadable } from "frontity";

// Codesplit the list component so it's not included if the users
// load a post directly.
export default loadable(() => import("./list"));

The actual rendering happens in list.js, which uses state.source.get(link) and its items field to build the list of posts.

// src/components/list/list.js
import { connect, styled, decode } from "frontity";
import Item from "./list-item";
import Pagination from "./pagination";

const List = ({ state }) => {
  // Get the data of the current list.
  const data = state.source.get(state.router.link);
  return (
    <Container>
      {/* If the list is a taxonomy, we render a title. */}
      {data.isTaxonomy && (
        <Header>
          {data.taxonomy}: {state.source[data.taxonomy][data.id].name}
        </Header>
      )}
      {/* If the list is an author, we render a title. */}
      {data.isAuthor && (
        <Header>Author: {state.source.author[data.id].name}</Header>
      )}
      {/* Iterate over the items of the list. */}
      {data.items.map(({ type, id }) => {
        const item = state.source[type][id];
        // Render one Item component for each one.
        return <Item key={item.id} item={item} />;
      })}
      <Pagination />
    </Container>
  );
};
export default connect(List);

Each item in the list is handled by the Item component in list-item.js, which renders the post’s clickable title, the author name, published date, and an optional <FeaturedMedia /> image.

// src/components/list/list-item.js
import { connect, styled } from "frontity";
import Link from "../link";
import FeaturedMedia from "../featured-media";

const Item = ({ state, item }) => {
  const author = state.source.author[item.author];
  const date = new Date(item.date);
  return (
    <article>
     {/* Rendering clickable post Title */}
      <Link link={item.link}>
        <Title dangerouslySetInnerHTML={{ __html: item.title.rendered }} />
      </Link>
      <div>
        {/* If the post has an author, we render a clickable author text. */}
        {author && (
          <StyledLink link={author.link}>
            <AuthorName>
              By <b>{author.name}</b>
            </AuthorName>
          </StyledLink>
        )}
        {/* Rendering post date */}
        <PublishDate>
          {" "}
          on <b>{date.toDateString()}</b>
        </PublishDate>
      </div>
      {/* If the want to show featured media in the
       * list of featured posts, we render the media. */}
      {state.theme.featured.showOnList && (
        <FeaturedMedia id={item.featured_media} />
      )}
      {/* If the post has an excerpt (short summary text), we render it */}
      {item.excerpt && (
        <Excerpt dangerouslySetInnerHTML={{ __html: item.excerpt.rendered }} />
      )}
    </article>
  );
};
// Connect the Item to gain access to `state` as a prop
export default connect(Item);

The list also includes a Pagination component so users can move between pages of results. It gets its props through the global context and is exported wrapped in connect().

// src/components/list/pagination.js
import { useEffect } from "react";
import { connect, styled } from "frontity";
import Link from "../link";

const Pagination = ({ state, actions }) => {
  // Get the total posts to be displayed based for the current link
  const { next, previous } = state.source.get(state.router.link);
  // Pre-fetch the the next page if it hasn't been fetched yet.
  useEffect(() => {
    if (next) actions.source.fetch(next);
  }, []);
  return (
    <div>
      {/* If there's a next page, render this link */}
      {next && (
        <Link link={next}>
          <Text>← Older posts</Text>
        </Link>
      )}
      {previous && next && " - "}
      {/* If there's a previous page, render this link */}
      {previous && (
        <Link link={previous}>
          <Text>Newer posts →</Text>
        </Link>
      )}
    </div>
  );
};
/**
 * Connect Pagination to global context to give it access to
 * `state`, `actions`, `libraries` via props
 */
export default connect(Pagination);

Single Posts

The Post component displays both single posts and pages. Structurally they’re the same, except that posts usually include metadata — author, date, categories — while pages don’t. In the component code, conditional statements render metadata only when data.isPost is true and when a featured image is selected in the theme’s state.

// src/components/post.js
import { useEffect } from "react";
import { connect, styled } from "frontity";
import Link from "./link";
import List from "./list";
import FeaturedMedia from "./featured-media";

const Post = ({ state, actions, libraries }) => {
  // Get information about the current URL.
  const data = state.source.get(state.router.link);
  // Get the data of the post.
  const post = state.source[data.type][data.id];
  // Get the data of the author.
  const author = state.source.author[post.author];
  // Get a human readable date.
  const date = new Date(post.date);
  // Get the html2react component.
  const Html2React = libraries.html2react.Component;

  useEffect(() => {
    actions.source.fetch("/");
    {/* Preloading the list component which runs only on mount */}
    List.preload();
  }, []);

  // Load the post, but only if the data is ready.
  return data.isReady ? (
    <Container>
      <div>
        <Title dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
        {/* Only display author and date on posts */}
        {data.isPost && (
          <div>
            {author && (
              <StyledLink link={author.link}>
                <Author>
                  By <b>{author.name}</b>
                </Author>
              </StyledLink>
            )}
            <DateWrapper>
              {" "}
              on <b>{date.toDateString()}</b>
            </DateWrapper>
          </div>
        )}
      </div>
      {/* Look at the settings to see if we should include the featured image */}
      {state.theme.featured.showOnPost && (
        <FeaturedMedia id={post.featured_media} />
      )}
      {/* Render the content using the Html2React component so the HTML is processed
       by the processors we included in the libraries.html2react.processors array. */}
      <Content>
        <Html2React html={post.content.rendered} />
      </Content>
    </Container>
  ) : null;
};
{/* Connect Post to global context to gain access to `state` as a prop. */} 
export default connect(Post);

The MarsLink component in src/components/link.js wraps Frontity’s base Link component. It accepts the same props, and — critically — it outputs an <a> element without forcing a full page reload, unlike a plain HTML anchor tag would.

// src/components/link.js
import { connect, useConnect } from "frontity";
import Link from "@frontity/components/link";

const MarsLink = ({ children, ...props }) => {
  const { state, actions } = useConnect();

  /** A handler that closes the mobile menu when a link is clicked. */
  const onClick = () => {
    if (state.theme.isMobileMenuOpen) {
      actions.theme.closeMobileMenu();
    }
  };

  return (
    <Link {...props} onClick={onClick} className={className}>
      {children}
    </Link>
  );
};
// Connect the Item to gain access to `state` as a prop
export default connect(MarsLink, { injectProps: false });

Navigation items are defined in frontity.settings.js and iterated over by the Nav component in nav.js, which matches each item’s URL and displays the resulting links inside the header. Separate menu.js and menu-modal.js components exist for mobile.

// src/components/nav.js
import { connect, styled } from "frontity";
import Link from "./link";

const Nav = ({ state }) => (
  <NavContainer>
    // Iterate over the menu exported from state.theme and menu items value set in frontity.setting.js
    {state.theme.menu.map(([name, link]) => {
      // Check if the link matched the current page url
      const isCurrentPage = state.router.link === link;
      return (
        <NavItem key={name}>
          {/* If link URL is the current page, add `aria-current` for a11y */}
          <Link link={link} aria-current={isCurrentPage ? "page" : undefined}>
            {name}
          </Link>
        </NavItem>
      );
    })}
  </NavContainer>
);
// Connect the Item to gain access to `state` as a prop
export default connect(Nav);

Styling Within a Frontity Theme

Frontity ships with styling support for both styled-components and Emotion, a CSS-in-JS library. You can style any React component with the styled function, appending the tag name and passing template literal CSS:

// Creating Button styled component
import { styled } from "frontity"

const Button = styled.div`
  background: lightblue;
  width: 100%;
  text-align: center;
  color: white;
`

Styled components can extend other components so long as the child accepts className props:

// mars-theme/src/components/header.js 
// ...
// We create a variable to use later as an example
Const LinkColor = "green";

// ... 

// Defining StyledLink component that is a third-party Link component
const StyledLink = styled(Link)`
  text-decoration: none;
  Background-color: ${linkColor};
`;

Inline styling also works through the css prop, which — unlike styled — returns a special object rather than a rendered component:

/* Using as CSS prop */
import { css } from "frontity";

const PinkButton = () => (
  <div css={css`background: pink`}>
    My Pink Button
  </div>
);

For site-wide CSS, Frontity provides a <Global /> component. It takes a css function with standard CSS rules inside. Frontity recommends using it for global HTML tags like <html>, <body>, <a> and <img>, and suggests adding it to the <Theme /> root component.

// packages/mars-theme/src/components/index.js
// ...

import { Global, css, styled } from "frontity";
import Title from "./title";
import Header from "./header";
// ...

// Theme root
const Theme = ({ state }) => {
  // Get information about the current URL.
  const data = state.source.get(state.router.link);

  return (
   <>
     {/* Add some metatags to the <head> of the HTML. */}
      <Title />
        // ...
      {/* Add global styles */}
      <Global styles={globalStyles} />
      {/* Add the header of the site. */}
      <HeadContainer>
        <Header />
      </HeadContainer>
        // ...
   </>
  );
 };

export default connect(Theme);

const globalStyles = css`
  body {
    margin: 0;
    font-family: -apple-system, "Helvetica Neue", Helvetica, sans-serif;
  }
  a,
  a:visited {
    color: inherit;
    text-decoration: none;
  }
`;

const HeadContainer = styled.div`
  // ...
`;

Customizing the Mars Theme

To demonstrate the real-world workflow, we put the techniques above to work in a live project. The full code is available in the companion GitHub repository.

Renaming the Package

The first step was changing the @frontity/mars-theme package to a custom name, @frontity/labre-theme. This requires three edits: the name property in the theme’s package.json, the name of the project folder in the same file, and both references in frontity.settings.js.

Screenshot of the package.json file open in VS Code. The left panel shows the files and the right panel displays the code.
I renamed my project from mars-theme to labre-theme in my package.json file,.

After those edits, reinitialize the project by deleting node_modules and reinstalling packages:

rm -rf node_modules
yarn install

Dynamic Menu Fetching

By default, menu items are hard-coded in frontity.settings.js. To fetch them dynamically from WordPress instead, first install the WP-REST-API V2 Menus plugin, which exposes menu routes like /menus/v1/menus/<slug> to the REST API.

If we check our project site at /wp-json/menu/v1/menus, it should display our selected menu items in the JSON. We can get the menu items with the menu item’s slug property.

Then create a menu-handler.js file with a menuHandler function that runs only when the pattern /menu/:slug matches:

// src/components/handler/menu-handler.js
const menuHandler = {
  name: "menus",
  priority: 10,
  pattern: "/menu/:slug",
  func: async ({ link, params, state, libraries }) => {
    console.log("PARAMS:", params);
    const { slug } = params;

    // Fetch the menu data from the endpoint
    const response = await libraries.source.api.get({
      endpoint: `/menus/v1/menus/${slug}`,
    });

    // Parse the JSON to get the object
    const menuData = await response.json();

    // Add the menu items to source.data
    const menu = state.source.data[link];
    console.log(link);
    Object.assign(menu, {
      items: menuData.items,
      isMenu: true,
    });
  },
};

export default menuHandler;

Update the root component in /src/index.js to import and register the handler under the source property. The handler fires before SSR and stores menu items in state, where they can be rendered by the navigation components. A menuUrl property can act as a variable endpoint; changing it swaps the menu shown.

// src/components/nav.js
import { connect, styled } from "frontity";
import Link from "./link";

/** Navigation Component. It renders the navigation links */
const Nav = ({ state }) => {
  {/* Define menu-items constants here */}
  const items = state.source.get(`/menu/${state.theme.menuUrl}/`).items;

  return (
  <NavContainer>
    {items.map((item) => {
       return (
        <NavItem key={item.ID}>
           <Link link={item.url}>{item.title}</Link>
         </NavItem>
      );
    })}
  </NavContainer>
  );
};

export default connect(Nav);

const NavContainer = styled.nav`
  list-style: none;
  // ...

Restructuring the Theme Folder

We reorganized the theme’s files into separate folders for pages, posts, headers, footers and styles. Whenever you move files, you must update all import paths in the components that reference them — otherwise everything breaks.

Adding Pages, Footers and Widgets

The default Mars Theme renders pages and posts with the same component. By copying post.js into a new pages.js component and stripping out the metadata sections, we can separate the two templates:

// src/components/pages/index.js
import { loadable } from "frontity";

/** Codesplit the list component so it's not included
*   if the users load a post directly. */
export default loadable(() => import("./page"));

A custom footer component with a sitemap and attribution blurb is straightforward to add, though it depends on a separate <Widget /> component that also needs to exist in the same folder:

// src/components/footer/footer.js
import React from "react";
import { connect, styled } from "frontity";
import Widget from "./widget"

const Footer = () => {
  return (
  <>
    <Widget />
    <footer>
      <SiteInfo>
        Frontity LABRE Theme 2021 | {" "} Proudly Powered by {"  "}
        <FooterLinks href="https://wordpress.org/" target="_blank" rel="noopener">WordPress</FooterLinks>
        {"  "} and
        <FooterLinks href="https://frontity.org/" target="_blank" rel="noopener"> Frontity</FooterLinks>
      </SiteInfo>
    </footer>
    </>
  );
};

export default connect(Footer);
// ...

Refactoring the Header

Our custom header moves the site logo and title to the left, with navigation on the right — a simple change in layout and styling from the default top-aligned arrangement.

// src/components/header.js
import { connect, styled } from "frontity";
import Link from "./link";
import Nav from "./nav";
import MobileMenu from "./menu";
import logo from "./images/frontity.png"

const Header = ({ state }) => {
  return (
    <>
      <Container>
        <StyledLink link="/">
         {/* Add header logo*/}
          <Logo src={logo} />
          <Title>{state.frontity.title}</Title>
        </StyledLink>
          {/*<Description>{state.frontity.description}</Description> */}
          <Nav />
      </Container>
        <MobileMenu />
    </>
  );
};
// Connect the Header component to get access to the `state` in its `props`
export default connect(Header);

const Container = styled.div`
  width: 1000px;
  // ...
  `}
{/* Logo styled component */}
const Logo = styled.img`
  max-width: 30px;
  display: inline-block;
  border-radius: 15px;
  margin-right: 15px;
`;

// ...

Global Styles, Fluid Typography and Webfonts

The <Global> component can be defined in its own file and imported into the root. We added custom CSS properties with clamped font sizes for fluid typography:

// src/components/styles/globalStyles.js
:root {
  --wide-container: clamp(16rem, 90vw, 70rem);
  --normal-container: clamp(16rem, 90vw, 58rem);
}

For fonts, we used three Google typefaces — Source Sans Pro for the header, PT Serif for the body text, and PT Sans Narrow for metadata. Using @font-face instead of @import reduces HTTP requests, and the Google webfonts helper tool provides the correct self-hosted @font-face syntax:

/* source: google webfonts helper */
/* source-sans-pro-regular - latin */
@font-face {
  font-family: 'Source Sans Pro';
  font-style: normal;
  font-weight: 400;
  src: url('../fonts/source-sans-pro-v14-latin-regular.eot'); /* IE9 Compat Modes */
  src: local(''),
    url('../fonts/source-sans-pro-v14-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
    url('../fonts/source-sans-pro-v14-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
    url('../fonts/source-sans-pro-v14-latin-regular.woff') format('woff'), /* Modern Browsers */
    url('../fonts/source-sans-pro-v14-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
    url('../fonts/source-sans-pro-v14-latin-regular.svg#SourceSansPro') format('svg'); /* Legacy iOS */
}

Those font files went into a /fonts folder matching what the font-face.js component references. Both globalStyles.js and font-face.js are then imported simultaneously into the root <Theme /> component:

// src/components/index.js

// ...
import FontFace from "./styles/font-face";
import globalStyles from "./styles/globalStyles";

/** Theme is the root React component of our theme. The one we will export
 * in roots. */
const Theme = ({ state }) => {
  // Get information about the current URL.
  const data = state.source.get(state.router.link);

  return (
    <>
    // ...

    {/* Add some global styles for the whole site, like body or a's.
     *  Not classes here because we use CSS-in-JS. Only global HTML tags. */}
      <Global styles={globalStyles} />
      <FontFace />
      {/* Add the header of the site. */}
      // ...

export default connect(Theme);

 {/* delete original globalStyles css component */}

 // ...

Post Styling and Gutenberg Blocks

Metadata like author, date and categories looks better with icons. We copied the SVG-based entry-meta components from Frontity’s Twenty Nineteen theme and dropped them into our posts folder:

// src/components/list/list-item.js

import { connect, styled } from "frontity";
import Link from "../link";
import FeaturedMedia from "../featured-media";

// import entry-meta
import Author from "../entry-meta/author";
import PostedOn from "../entry-meta/posted-on";

const Item = ({ state, item }) => {

  return (
    <article>
      <div>
        {/* If the post has an author, we render a clickable author text. */}
        <EntryMeta>
          <Author authorId={item.author} /> {"|  "}
          <PostedOn post={item} />
        </EntryMeta>
      </div>

      <Link link={item.link}>
        <Title dangerouslySetInnerHTML={{ __html: item.title.rendered }} />
      </Link>
      // ...
    </article>
  );
};

// Connect the Item to gain access to `state` as a prop
export default connect(Item);

To get core styling for WordPress block content, we copied style.css and theme.css from Frontity’s Twenty Twenty theme into a /styles/gutenberg/ folder.

A post with DevTools open and highlighting the markup for the button component.
That .wp-block-buttons class is declared in the WordPress blocks stylesheet that we aren’t using… yet.

Those stylesheets are added to the theme root alongside the global styles. From there, overriding specific block styles — like .wp-block-buttons — is just a matter of targeting them inside a styled component.


The Mars Theme remains a solid foundation whether you are building a personal blog or a full editorial site. All the custom components and configuration files are available on GitHub for further exploration.