Building Gatsby Themes For WordPress-Powered Websites

Gatsby is an open-source framework built on top of React. With Gatsby, you can pull data from (almost) anywhere and use it to generate static or dynamic websites. The data can be pulled from a CMS, which definitely brings WordPress to the table. You get the advantages of a static website (speed, security, static hosting) while you continue to manage your content via a WordPress dashboard.

One of the particularities of Gatsby framework is that it proposes themes as a customization tool. As someone with a strong background in WordPress, I find the concept of Gatsby themes particularly appealing. I used to design and develop WordPress themes. However, with the growing interest in Jamstack solutions, I gradually shifted towards working with WordPress as a headless CMS. In this article, I would like to share some concepts that I have learned from this transition.

Note: Before we go any further, let’s focus on the tools that we are going to use. Gatsby provides an official gatsby-source-wordpress plugin. To make it work, we need to prepare our WordPress end. More precisely, we need to expose the Gatsby-flavored WordPress data via a GraphQL API. In practice, that means installing two WordPress plugins WPGraphQL and WPGatsby. Both are available via the official WordPress plugins repository and do not require any configuration.

What Are Gatsby Themes?

Gatsby theme is a bunch of shared functionalities abstracted within a Node.js package. A theme is therefore destined to be published (to a registry like npm) and reused as an installable dependency.

Since we are talking Gatsby and WordPress here, I will clarify it straight away — there are similarities with WordPress themes, but we should not equate the notion of WordPress themes with Gatsby themes. For someone with a WordPress background (like myself), the dissociation may be challenging at the beginning.

A WordPress theme is a mandatory system of templates that defines what we see on the front end. The responsibility of a good WordPress theme ends here. It should not introduce any functionalities since functionalities are the plugins’ territory. There is, therefore, a strict separation between themes and plugins in the WordPress ecosystem. Themes should take care of the presentation layer, and plugins take care of the functional aspects.

Following the Gatsby definition, themes are responsible for functionalities. Shouldn’t we call them plugins then? Actually, Gatsby, like WordPress, has both plugins and themes. Plugins, just like themes, are installable Node.js packages that implement Gatsby APIs. And in fact, a Gatsby theme is a Gatsby plugin. If a plugin owns a section, a page, or part of a page on a site — we call it a theme.

Moreover, unlike WordPress, Gatsby does not require using themes to build a site. Instead, you would probably start creating your site by setting up a project structured as below:

That’s ok until you have more than one site to maintain. In that case, you might want to abstract the common parts of the process and manage (version and update) them separately.

Thanks to Gatsby theming system, you can bundle the shared parts into a package (or multiple packages), publish the packages and finally install them throughout numerous applications. Note, that I used the plural form packages — you can combine multiple themes within a project.

Child Themes And Shadowing

When working with Gatsby and WordPress, you will identify some core functionalities that are common for all projects. I mean here: sourcing the data and building the pages dynamically. It seems worthwhile to have a theme that takes care of the data sourcing logic and the creation of the pages. On the other hand, how you decide to display your pages may change from one project to another. Whatever you set at the core level, you will probably need to override at some point.

One of the possible approaches is to have a core (parent) theme and build child themes on top of the core one.

What do I mean by a Gatsby child theme?

Let’s proceed with a comparison of WordPress child themes. WordPress child themes allow us to add functionalities and override templates. They provide a safe way to enhance and modify an existing theme.

A Gatsby child theme uses a parent theme as its plugin. We can then use the concept of shadowing that gives the child theme the capability to override the parent theme files; that’s similar to overriding WordPress templates in a child theme. Shadowing means that we can override files from the src directory included in the webpack bundle. It’s worth underlining that shadowing is possible on the project level (where we consume our themes as packages). We’ll see it in action later in this article.

With WordPress, we are limited to only one parent theme, only one child theme, and no further chaining is possible. With the flexibility of Gatsby themes, we can go much further. It’s possible to build different configurations of child-parent chains.

Let’s now see Gatsby theming in action. In our example, we will build two themes, gatsby-theme-wp-parent and its child-theme gatsby-theme-wp-child. I chose this setup for the sake of simplicity. In a real-world scenario, you might want to decompose your functionalities into more themes, each one with some specific responsibility.

We will publish our themes, install them in a project, and add further customization via project-level shadowing.

Development Setup

The last illustration depicts the structure of the final user’s project (site), where the themes are consumed. They are installed as the project’s dependencies. This setup assumes that the themes are available via some npm repository, which means we’ve already published them. We’re not there yet. We need to build the parent and child themes first. But what does the development setup looks like? Our themes are two independent packages, but we need to work on them in parallel within a single project during the development. Moreover, we want to set up a demo within the same project that implements the themes directly.

One of the possible solutions is yarn workspaces. With yarn workspaces, we work within a single mono-repo with a single lock file at the project-root level. Moreover, the dependencies can be linked together, which means that the workspaces depend on one another, and we use the local versions during development.

How to set up yarn workspaces? First, make sure to have yarn installed globally. Next, at the root of your monorepo, add the package.json file that specifies the workspaces:

{
  "private": true,
  "workspaces": [
    "packages/*",
    "demo"
  ]
}

Now, each theme is a subfolder within packages with its own package.json file and an empty main entry index.js. I proceed like so with each theme I add:

mkdir packages/gatsby-theme-wp-parent
touch packages/gatsby-theme-wp-parent/package.json packages/gatsby-theme-wp-parent/index.js

With the package.json as follows:

{
  "name": "@pehaa/gatsby-theme-wp-parent",
  "version": "1.0.0",
  "license": "MIT",
  "main": "index.js"
}

We’ll discuss the theme publishing a little bit further. But, for the moment, let’s note that we will publish our themes as scoped packages; I use my nickname @pehaa as a scope here. Remember that, if you decide to publish scoped packages to the public npm registry https://registry.npmjs.org, you must state the public access explicitly and add the following to their package.json files:

"publishConfig": {
  "access": "public"
}

In addition to themes, we will also need a demo workspace from which we will try out our code. The demo has to be a "private" package since it is not supposed to be published.

// demo/package.json
{
  "private": true,
  "name": "demo",
  "version": "1.0.0",
  "scripts": {
    "build": "gatsby build",
    "develop": "gatsby develop",
    "clean": "gatsby clean"
  }
}

With the workspaces setup, we can run the develop or build scripts from anywhere in our monorepo by specifying the script and the workspace like so:

yarn workspace demo develop

By the way, you are not limited to a single demo. For example, our GatsbyWPThemes monorepo contains multiple demos that we add to the examples directory. In this case, the root-level package.json file defines workspaces as follows:

"workspaces": [
  "packages/*",
  "examples/*"
]
Building Gatsby Themes

First of all, we need to install react, react-dom and gatsby. We need to install these three as peer dependencies (-P) in each theme and as dependencies in our demo. We also install the parent theme as the child’s theme dependency and the child theme as the demo’s dependency.

yarn workspace @pehaa/gatsby-theme-wp-parent add -P react react-dom gatsby
yarn workspace @pehaa/gatsby-theme-wp-child add -P react react-dom gatsby
yarn workspace @pehaa/gatsby-theme-wp-child add "@pehaa/gatsby-theme-wp-parent@"
yarn workspace demo add react react-dom gatsby "@pehaa/gatsby-theme-wp-child@"

Note: You can’t add @pehaa/gatsby-theme-wp-parent or @pehaa/gatsby-theme-wp-child without a version number. You must specify it as either `@or@1.0.0. Without it, npm will try to fetch the package from the repository instead of using the local one. Later on, when we publish our packages with Lerna, all the` will be automatically updated to the current theme versions and kept in sync.

Parent Theme

Let’s now focus on the parent theme and its dependencies:

yarn workspace @pehaa/gatsby-theme-wp-parent add gatsby-source-wordpress gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharp gatsby-awesome-pagination

Our parent theme’s responsibility is to load the source plugin and three plugins required for processing and displaying images. We load them all in the gatsby-config.js file.

// gatsby-config.js
module.exports = (options) => {
  return {
    plugins: [
      'gatsby-plugin-sharp', // must have for gatsby
      'gatsby-transformer-sharp', // must have for gatsby images
      'gatsby-plugin-image',
      {
        resolve: 'gatsby-source-wordpress',
        options: {
          url: `${options.wordPressUrl}/graphql`,
        },
      },
    ],
  }
}

Besides sourcing the content, we need to build routes for our WordPress content dynamically. We need to create routes for WordPress static pages, individual posts, blog archive, category archive, and tags archive. Gatsby provides the createPages API as a part of Gatsby Node API. Let’s take a look at the code responsible for the creation of individual posts.

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  const postsQuery = await graphql(query GET_POSTS {
      allWpPost(sort: {order: DESC, fields: date}) {
        edges {
          node {
            uri
            id
          }
        }
      }
    })
  const posts = postsQuery.data.allWpPost.edges
  posts.forEach(({ node }) => {
    createPage({
      path: node.uri, 
      component: path.resolve('../src/templates/post-query.js'),
      context: {
        // Data passed to context is available in page queries as GraphQL variables
        // we need to add the post id here
        // so our blog post template knows which blog post it should display
        id: node.id
      },
    })
  })
}

You can find the complete code in this GitHub repository. You may notice that it varies depending on the page type. It is different for a post, a page, or an archive, especially with the pagination implemented for the latter. Still, it follows the same pattern:

  • run an async graphql “get items” query;
  • loop over the resulting items and run the createPage helper function for each item, passing:
    • the path,
    • component — the template file; Gatsby has to know what each page should display,
    • context — whatever data the template (provided in the component field) might need.

Since we do not want to worry about the UI part within the parent theme — we delegate it to the component that we will shadow in the child theme.

// src/templates/post-query.js
import { graphql } from "gatsby"
import Post from "../components/Post" 
export default Post 

export const pageQuery = graphql`
  query ($id: String!) {
    wpPost(id: { eq: $id }) {
      # query all usefull data
    } 
  }
`

The Post component has access to the data from the graphql page query defined in the template file. Our component receives the query results via props as props.data. Our component file is separated from the template but has access to its data. With this setup, we are able to shadow Post component without having to rewrite the query.

// src/components/Post.js
import React from 'react'
const Post = (props) => {
  return <pre>{JSON.stringify(props.data, null, 2)}</pre>
}
export default Post

Child Theme

Now, let’s move on to the child theme and add its dependencies.

Note: I chose to use Chakra UI as components library, it’s based on emotion and comes with its own Gatsby plugin. We also need to install WordPress-content-specific styles from @wordpress/block-library.

yarn workspace @pehaa/gatsby-theme-wp-child add @chakra-ui/gatsby-plugin @chakra-ui/react @emotion/react @emotion/styled @wordpress/block-library framer-motion gatsby-plugin-webfonts html-react-parser

The child theme’s responsibility is the UI part, and we need to override the bare bone output generated by the parent theme. For the shadowing to work, we need to follow the files structure from the parent theme. For example, to override the Post component from gatsby-theme-wp-parent/src/components/Post.js we need to create a Post.js file in gatsby-theme-wp-child/src/@pehaa/gatsby-theme-wp-parent/components. The @pehaa intermediate folder corresponds to the scope of the gatsby-theme-wp-parent package.

Passing Options To Themes

We load and configure gatsby plugins in a gatsby-config.js file. We will have three config files in our setup, one on each level, our parent theme, the child theme, and the demo.

├── demo
│      └── gatsby-config.js
├── packages
│   ├── gatsby-theme-wp-child
│   │   └── gatsby-config.js
│   └── gatsby-theme-wp-parent
│       └── gatsby-config.js
└── ...

On the demo level, the config loads the child theme like so:

// demo/gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: '@pehaa/gatsby-theme-wp-child',
      options: {
        wordPressUrl: process.env.GATSBY_WP_URL,
        /* other options */
      },
    },
  ],
}

As you can see above, we pass options to the child theme. These will be available within the config file on the child theme’s level. That’s possible since Gatsby plugins have config exported as a function. Thus, when we load a plugin providing some options, the plugin receives them as an argument of its config function. In particular, the options that we pass to a theme can be “forwarded” to its parent-level theme like so:

// gatsby-theme-wp-child/gatsby-config.js
const defaultFonts  = ...
module.exports = (options) => {
  // destructure option to extract fonts 
  const {fonts, ...rest} = options
  return {
    plugins: [
      {
        resolve: @pehaa/gatsby-theme-wp-parent,
        options: {
          // "forward" the options gatsby-theme-wp-child options to its parent theme
          ...rest
        }
      },
      '@chakra-ui/gatsby-plugin',
      {
        resolve: gatsby-plugin-webfonts,
        options: {
          fonts: fonts || defaultFonts
        },
      },
    ],
  }
}

Let’s look again at the code above. Note that we define font faces on the child-theme level, but we keep the possibility to modify them via theme options.

// demo/gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `@pehaa/gatsby-theme-wp-child`,
      options: {
        wordPressUrl: process.env.GATSBY_WP_URL,
        fonts: {
          google: [{family: "Rubik"}],
        },
      },
    },
  ],
}

When configuring our themes, we should remember that a theme is just a package, and the end-user does not directly access its code. Therefore, it’s a good idea to think ahead and expose proper settings. If our theme loads a plugin that requires configuration, we probably should pass the plugins options from the project (demo) level all the way down.

Let’s look into an example. Our parent theme uses the gatsby-source-wordpress plugin that fetches the data from WordPress. This plugin comes with a bunch of options, some of them possibly critical to the build process, like schema.requestConcurrency, or schema.timeout. But, again, the parent theme is just a package, and the end-user can not edit its gatsby-config file. It may seem obvious, but we somehow missed it in the initial release of Gatsby WP Themes. However, with a quick fix, the user can pass the gatsby-plugin-source-wordpress options from the project’s config…

// user's project gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `@pehaa/gatsby-theme-wp-child`,
      options: {
        wordPressUrl: process.env.GATSBY_WP_URL,
        gatsbySourceWordPressOptions: {},
        // ...
      },
    },
  ],
}

… via the child and parent theme to the destination plugin:

// packages/gatsby-theme-wp-parent/gatsby-config.js
module.exports = (options) => {
  return {
    plugins: [
      // ...
      {
        resolve: `gatsby-plugin-source-wordpress`,
        options: {
          url: `${options.wordPressUrl}/graphql`,
          ...options.gatsbySourceWordPressOptions
        },
      },
    ],
  }
}
CSS Theming

The CSS-in-JS solutions that support theming seem a good fit for Gatsby themes. Our Gatsby child theme will use Chakra UI framework, and we will slightly customize its CSS theme. Yes, Chakra UI also uses the notion of a “theme”. In this context, a theme is a JavaScript object that stores design system style values, scales, and/or design tokens. To avoid any confusion, I will refer to it as a “CSS theme”. We’ve already installed the required @chakra-ui packages together with the Gatsby plugin @chakra-ui/gatsby-plugin. Let’s explore the plugin’s code to find out how it works. It actually wraps our Gatsby application into the ChakraProvider and exposes the src/theme.js file for shadowing, so that we can proceed like so:

/ packages/gatsby-theme-wp-child/src/@chakra-ui/gatsby-plugin/theme.js /
import { extendTheme } from "@chakra-ui/react"
const theme = {
  fonts: {
    body: "Karma, sans-serif",
    heading: "Poppins, sans-serif",
  },
  styles: {
    global: {
      body: {
        color: "gray.700",
        fontSize: "xl",
      },
    },
  },
  components: {
    Button: {
      baseStyle: {
        borderRadius: "3xl",
      },
      defaultProps: {
        colorScheme: "red",
      },
    },
  },
}
export default extendTheme(theme)

Once again, we used the concept of shadowing. The key aspect here is the location where we created the theme.js file.

Later on, we’ll see how to shadow the CSS theme on the user’s project level.

Publishing Themes With Lerna

Once your themes are ready, you need to publish them. If you want to share your code publicly, you will most probably publish it to the public npm registry. And if you’ve never published a package before, you can get familiar with the process by playing with Verdaccio on your local machine.

At Gatsby WP Themes, we use a premium service from Cloudsmith. Cloudsmith supports fully-featured registries for npm packages with the premium option for private registries and a free solution for the public ones. I will continue with a free Cloudsmith solution. Once you have created your account, create a new repository; mine is pehaa/gatsby-wp-theming.

In order to make changes to your Cloudsmith registry via your command line, you will need to provide your login credentials for this registry. Just type:

and you will be asked for your username, your password (which is your API KEY), and your email.

With a multi-package git repository, you might want to use Lerna to facilitate the publishing. Lerna matches well with yarn workspaces. You can install Lerna CLI globally with npm install --global lerna. To initiate it within our project, we’ll run the following command:

lerna init --independent

The command above will create a lerna.json file in the root of the monorepo. You’ll need to add the "useWorkspaces" : true and "npmClient": "yarn" manually; you may also need to specify the command.publish.registry if it’s not the default public npm one.

{
  "npmClient": "yarn",
  "useWorkspaces": true,
  "version": "independent",
  "command": {
    "publish": {
      "registry": "https://cloudsmith.io/organisation/repository_name"
    }
  }
}

Then, the lerna publish command publishes packages that have changed since the last release. By default, Lerna runs prompts for the version change of each package being updated. You can skip the prompts by running:

lerna publish [major|minor|patch|premajor|preminor|prepatch|prerelease] --yes

You can also configure Lerna to use the Conventional Commits Specification to determine the version bump and generate CHANGELOG.md files. With all the options available, you should be able to adapt the way you use Lerna to your workflow.

Using A Theme In A Project

Now, let’s stop the development server and take the user’s perspective. We will create a new project, gatsby-wp-site, that implements gatsby-theme-wp-child as a package installed from the npm repository. Within our project folder, we will install our four dependencies: gatsby, react, react-dom, and the theme itself. Since we used Cloudsmith to publish @pehaa-scoped packages, we’ll have to add a .npmrc file where we specify @pehaa-scoped repository like so:

mkdir gatsby-wp-site
cd gatsby-wp-site
echo "@pehaa:registry=https://npm.cloudsmith.io/pehaa/gatsby-wp-theming/" >> .npmrc
yarn init -yp
yarn add react react-dom gatsby @pehaa/gatsby-theme-wp-child

Our site is almost ready. We only have to create a gatsby-config.file to load the theme and provide the WordPress URL. Once it’s done, we are ready to run gatsby build.

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: "@pehaa/gatsby-theme-wp-child",
      options: {
        wordPressUrl: "https://yourwordpress.website"
      }
    }
  ]
}

Our site is ready.

What about customization? We can still take advantage of shadowing. Moreover, the project level always takes precedence in terms of shadowing. Let’s see it in action by overriding the Footer component. Right now, our footer is defined in @pehaa/gatsby-theme-wp-child/src/components/Footer.js. We need to create the src folder and recreate the following files structure:

gatsby-wp-site
├── src
│   └── @pehaa
│       └── gatsby-theme-wp-child
│           └── components
│               └── Footer.js

With the above files structure, we are ready to provide a new version of the site footer. For example:

import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import { Box } from "@chakra-ui/react"
const Footer = () => {
  const data = useStaticQuery(graphqlquery {
      wp {
        generalSettings {
          title
        }
      }
    })
  return (
    <Box
      as="footer"
      p="6"
      fontSize="sm"
      bg="gray.700"
      color="white"
      mt="auto"
      textAlign="center"
    >
      <b>{data.wp.generalSettings.title}</b> - Built with WordPress and GatsbyJS
    </Box>
  )
}
export default Footer

Finally, let’s see how we can work with the CSS theme. With the code as below, properly located in src/@chakra-ui/gatsby-plugin/theme.js you can extend the default theme within the project.

// src/@chakra-ui/gatsby-plugin/theme.js
import { extendTheme } from "@chakra-ui/react"
const theme = {
  /* ... */
}
export default extendTheme(theme)

In most cases, this is not exactly what you need. The new CSS theme ignores the one from gatsby-theme-wp-child, whereas you’d instead want to extend the CSS theme set in the Gatsby child theme. The latter is possible since the extendTheme function allows you to pass multiple objects. To make it work, you have to import the CSS theme from gatsby-theme-wp-child and pass it as the second argument to the extendTheme function:

// src/@chakra-ui/gatsby-plugin/theme.js
import theme from "@pehaa/gatsby-theme-wp-child/src/@chakra-ui/gatsby-plugin/theme"
import { extendTheme } from "@chakra-ui/react"
const extendedTheme = {
  fonts: {
    body: "Rubik, sans-serif",
    heading: "Rubik, sans-serif",
  },
  / ... /
}
export default extendTheme(extendedTheme, theme)

You can see the site live here, it’s deployed from the main branch of this GitHub repo.

Wrapping Up

You’ve just seen Gatsby theming in action. With the theme approach, you can quickly set up multiple Gatsby sites with most of their code maintained within the theme packages. We’ve also seen how to separate parts of the project into packages and how to take advantage of shadowing.

In our example, we’ve followed the two-themes setup with a parent-child relationship between the themes. This may not always be an ideal choice.

Sometimes, you might want to go pretty far with the UI customization. In that case, you might consider loading and shadowing directly the parent theme instead of using the child one. In a real-world scenario, you would probably opt for a few child-level themes responsible for different, reusable parts of the UI (e.g. comments, forms or search).

Further Reading On Smashing Magazine

Native Search vs. Jetpack Instant Search in Headless WordPress With Gatsby

Have you already tried using WordPress headlessly with Gatsby? If you haven’t, you might check this article around the new Gatsby source plugin for WordPress; gatsby-source-wordpress is the official source plugin introduced in March 2021 as a part of the Gatsby 3 release. It significantly improves the integration with WordPress. Also, the WordPress plugin WPGraphQL providing the GraphQL API is now available via the official WordPress repository.

With stable and maintained tools, developing Gatsby websites powered by WordPress becomes easier and more interesting. I got myself involved in this field, I co-founded (with Alexandra Spalato), and recently launched Gatsby WP Themes — a niche marketplace for developers building WordPress-powered sites with Gatsby. In this article, I would love to share my insights and, in particular, discuss the search functionality.

Search does not come out of the box, but there are many options to consider. I will focus on two distinct possibilities — taking advantage of WordPress native search (WordPress search query) vs. using Jetpack Instant Search.

Getting started

Let’s start by setting up a WordPress-powered Gatsby website. For the sake of simplicity, I will follow the getting started instructions and install the gatsby-starter-wordpress-blog starter.

gatsby new gatsby-wordpress-w-search https://github.com/gatsbyjs/gatsby-starter-wordpress-blog

This simple, bare-bone starter creates routes exclusively for individual posts and blog pages. But we can keep it that simple here. Let’s imagine that we don’t want to include pages within the search results.

For the moment, I will leave the WordPress source website as it is and pull the content from the starter author’s WordPress demo. If you use your own source, just remember that there are two plugins required on the WordPress end (both available via the plugin repository):

  • WPGraphQL – a plugin that runs a GraphQL server on the WordPress instance
  • WPGatsby – a plugin that modifies the WPGraphQL schema in Gatsby-specific ways (it also adds some mechanism to optimize the build process)

Setting up Apollo Client

With Gatsby, we usually either use the data from queries run on page creation (page queries) or call the useStaticQuery hook. The latter is available in components and does not allow dynamic query parameters; its role is to retrieve GraphQL data at build time. None of those two query solutions works for a user’s-initiated search. Instead, we will ask WordPress to run a search query and send us back the results. Can we send a graphQL search query? Yes! WPGraphQL provides search; you can search posts in WPGraphQL like so:

posts(where: {search: "gallery"}) {
  nodes {
    id
    title
    content
  }
}

In order to communicate directly with our WPGraphQL API, we will install Apollo Client; it takes care of requesting and caching the data as well as updating our UI components.

yarn add @apollo/client cross-fetch

To access Apollo Client anywhere in our component tree, we need to wrap our app with ApolloProvider. Gatsby does not expose the App component that wraps around the whole application. Instead, it provides the wrapRootElement API. It’s a part of the Gatsby Browser API and needs to be implemented in the gatsby-browser.js file located at the project’s root.

// gatsby-browser.js
import React from "react"
import fetch from "cross-fetch"
import { ApolloClient, HttpLink, InMemoryCache, ApolloProvider } from "@apollo/client"
const cache = new InMemoryCache()
const link = new HttpLink({
  /* Set the endpoint for your GraphQL server, (same as in gatsby-config.js) */
  uri: "https://wpgatsbydemo.wpengine.com/graphql",
  /* Use fetch from cross-fetch to provide replacement for server environment */
  fetch
})
const client = new ApolloClient({
  link,
  cache,
})
export const wrapRootElement = ({ element }) => (
  <ApolloProvider client={client}>{element}</ApolloProvider>
)

SearchForm component

Now that we’ve set up ApolloClient, let’s build our Search component.

touch src/components/search.js src/components/search-form.js src/components/search-results.js src/css/search.css

The Search component wraps SearchForm and SearchResults

// src/components/search.js
import React, { useState } from "react"
import SearchForm from "./search-form"
import SearchResults from "./search-results"

const Search = () => {
  const [searchTerm, setSearchTerm] = useState("")
  return (
    <div className="search-container">
      <SearchForm setSearchTerm={setSearchTerm} />
      {searchTerm && <SearchResults searchTerm={searchTerm} />}
    </div>
  )
}
export default Search

<SearchForm /> is a simple form with controlled input and a submit handler that sets the searchTerm state value to the user submission.

// src/components/search-form.js
import React, { useState } from "react"
const SearchForm = ({ searchTerm, setSearchTerm }) => {
  const [value, setValue] = useState(searchTerm)
  const handleSubmit = e => {
    e.preventDefault()
    setSearchTerm(value)
  }
  return (
    <form role="search" onSubmit={handleSubmit}>
      <label htmlFor="search">Search blog posts:</label>
      <input
        id="search"
        type="search"
        value={value}
        onChange={e => setValue(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  )
}
export default SearchForm

The SearchResults component receives the searchTerm via props, and that’s where we use Apollo Client.

For each searchTerm, we would like to display the matching posts as a list containing the post’s title, excerpt, and a link to this individual post. Our query will be like so:

const GET_RESULTS = gql`
  query($searchTerm: String) {
    posts(where: { search: $searchTerm }) {
      edges {
        node {
          id
          uri
          title
          excerpt
        }
      }
    }
  }
`

We will use the useQuery hook from @apollo-client to run the GET_RESULTS query with a search variable.

// src/components/search-results.js
import React from "react"
import { Link } from "gatsby"
import { useQuery, gql } from "@apollo/client"
const GET_RESULTS = gql`
  query($searchTerm: String) {
    posts(where: { search: $searchTerm }) {
      edges {
        node {
          id
          uri
          title
          excerpt
        }
      }
    }
  }
`
const SearchResults = ({ searchTerm }) => {
  const { data, loading, error } = useQuery(GET_RESULTS, {
    variables: { searchTerm }
  })
  if (loading) return <p>Searching posts for {searchTerm}...</p>
  if (error) return <p>Error - {error.message}</p>
  return (
    <section className="search-results">
      <h2>Found {data.posts.edges.length} results for {searchTerm}:</h2>
      <ul>
        {data.posts.edges.map(el => {
          return (
            <li key={el.node.id}>
              <Link to={el.node.uri}>{el.node.title}</Link>
            </li>
          )
        })}
      </ul>
    </section>
  )
}
export default SearchResults

The useQuery hook returns an object that contains loading, error, and data properties. We can render different UI elements according to the query’s state. As long as loading is truthy, we display <p>Searching posts...</p>. If loading and error are both falsy, the query has completed and we can loop over the data.posts.edges and display the results.

if (loading) return <p>Searching posts...</p>
if (error) return <p>Error - {error.message}</p>
// else
return ( //... )

For the moment, I am adding the <Search /> to the layout component. (I’ll move it somewhere else a little bit later.) Then, with some styling and a visible state variable, I made it feel more like a widget, opening on click and fixed-positioned in the top right corner.

Paginated queries

Without the number of entries specified, the WPGraphQL posts query returns ten first posts; we need to take care of the pagination. WPGraphQL implements the pagination following the Relay Specification for GraphQL Schema Design. I will not go into the details; let’s just note that it is a standardized pattern. Within the Relay specification, in addition to posts.edges (which is a list of { cursor, node } objects), we have access to the posts.pageInfo object that provides:

  • endCursor – cursor of the last item in posts.edges,
  • startCursor – cursor of the first item in posts.edges,
  • hasPreviousPage – boolean for “are there more results available (backward),” and
  • hasNextPage – boolean for “are there more results available (forward).”

We can modify the slice of the data we want to access with the additional query variables:

  • first – the number of returned entries
  • after – the cursor we should start after

How do we deal with pagination queries with Apollo Client? The recommended approach is to use the fetchMore function, that is (together with loading, error and data) a part of the object returned by the useQuery hook.

// src/components/search-results.js
import React from "react"
import { Link } from "gatsby"
import { useQuery, gql } from "@apollo/client"
const GET_RESULTS = gql`
  query($searchTerm: String, $after: String) {
    posts(first: 10, after: $after, where: { search: $searchTerm }) {
      edges {
        node {
          id
          uri
          title
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`
const SearchResults = ({ searchTerm }) => {
  const { data, loading, error, fetchMore } = useQuery(GET_RESULTS, {
    variables: { searchTerm, after: "" },
  })
  if (loading && !data) return <p>Searching posts for {searchTerm}...</p>
  if (error) return <p>Error - {error.message}</p>
  const loadMore = () => {
    fetchMore({
      variables: {
        after: data.posts.pageInfo.endCursor,
      },
      // with notifyOnNetworkStatusChange our component re-renders while a refetch is in flight so that we can mark loading state when waiting for more results (see lines 42, 43)
      notifyOnNetworkStatusChange: true,
    })
  }

  return (
    <section className="search-results">
      {/* as before */}
      {data.posts.pageInfo.hasNextPage && (
        <button type="button" onClick={loadMore} disabled={loading}>
          {loading ? "Loading..." : "More results"}
        </button>
      )}
    </section>
  )
}
export default SearchResults

The first argument has its default value but is necessary here to indicate that we are sending a paginated request. Without first, pageInfo.hasNextPage will always be false, no matter the search keyword.

Calling fetchMore fetches the next slice of results but we still need to tell Apollo how it should merge the “fetch more” results with the existing cached data. We specify all the pagination logic in a central location as an option passed to the InMemoryCache constructor (in the gatsby-browser.js file). And guess what? With the Relay specification, we’ve got it covered — Apollo Client provides the relayStylePagination function that does all the magic for us.

// gatsby-browser.js
import { ApolloClient, HttpLink, InMemoryCache, ApolloProvider } from "@apollo/client"
import { relayStylePagination } from "@apollo/client/utilities"
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        posts: relayStylePagination(["where"]),
      },
    },
  },
})
/* as before */

Just one important detail: we don’t paginate all posts, but instead the posts that correspond to a specific where condition. Adding ["where"] as an argument to relayStylePagination creates a distinct storage key for different search terms.

Making search persistent

Right now my Search component lives in the Layout component. It’s displayed on every page but gets unmounted every time the route changes. What if we could keep the search results while navigating? We can take advantage of the Gatsby wrapPageElement browser API to set persistent UI elements around pages.

Let’s move <Search /> from the layout component to the wrapPageElement:

// gatsby-browser.js
import Search from "./src/components/search"
/* as before */
export const wrapPageElement = ({ element }) => {
  return <><Search />{element}</>
}

The APIs wrapPageElement and wrapRootElement exist in both the browser and Server-Side Rendering (SSR) APIs. Gatsby recommends that we implement wrapPageElement and wrapRootElement in both gatsby-browser.js and gatsby-ssr.js. Let’s create the gatsby-ssr.js (in the root of the project) and re-export our elements:

// gatsby-ssr.js
export { wrapRootElement, wrapPageElement } from "./gatsby-browser"

I deployed a demo where you can see it in action. You can also find the code in this repo.

The wrapPageElement approach may not be ideal in all cases. Our search widget is “detached” from the layout component. It works well with the position “fixed” like in our working example or within an off-canvas sidebar like in this Gatsby WordPress theme.

But what if you want to have “persistent” search results displayed within a “classic” sidebar? In that case, you could move the searchTerm state from the Search component to a search context provider placed within the wrapRootElement:

// gatsby-browser.js
import SearchContextProvider from "./src/search-context"
/* as before */
export const wrapRootElement = ({ element }) => (
  <ApolloProvider client={client}>
    <SearchContextProvider>
      {element}
    </SearchContextProvider>
  </ApolloProvider>
)

…with the SearchContextProvider defined as below:

// src/search-context.js
import React, {createContext, useState} from "react"
export const SearchContext = createContext()
export const SearchContextProvider = ({ children }) => {
  const [searchTerm, setSearchTerm] = useState("")
  return (
    <SearchContext.Provider value={{ searchTerm, setSearchTerm }}>
      {children}
    </SearchContext.Provider>
  )
}

You can see it in action in another Gatsby WordPress theme:

Note how, since Apollo Client caches the search results, we immediately get them on the route change.

Results from posts and pages

If you checked the theme examples above, you might have noticed how I deal with querying more than just posts. My approach is to replicate the same logic for pages and display results for each post type separately.

Alternatively, you could use the Content Node interface to query nodes of different post types in a single connection:

const GET_RESULTS = gql`
  query($searchTerm: String, $after: String) {
    contentNodes(first: 10, after: $after, where: { search: $searchTerm }) {
      edges {
        node {
          id
          uri
          ... on Page {
            title
          }
          ... on Post {
            title
            excerpt
          }
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
`

Our solution seems to work but let’s remember that the underlying mechanism that actually does the search for us is the native WordPress search query. And the WordPress default search function isn’t great. Its problems are limited search fields (in particular, taxonomies are not taken into account), no fuzzy matching, no control over the order of results. Big websites can also suffer from performance issues — there is no prebuilt search index, and the search query is performed directly on the website SQL database.

There are a few WordPress plugins that enhance the default search. Plugins like WP Extended Search add the ability to include selected meta keys and taxonomies in search queries.

The Relevanssi plugin replaces the standard WordPress search with its search engine using the full-text indexing capabilities of the database. Relevanssi deactivates the default search query which breaks the WPGraphQL where: {search : …}. There is some work already done on enabling Relevanssi search through WPGraphQL; the code might not be compatible with the latest WPGraphQL version, but it seems to be a good start for those who opt for Relevanssi search.

In the second part of this article, we’ll take one more possible path and have a closer look at the premium service from Jetpack — an advanced search powered by Elasticsearch. By the way, Jetpack Instant search is the solution adopted by CSS-Tricks.

Using Jetpack Instant Search with Gatsby

Jetpack Search is a per-site premium solution by Jetpack. Once installed and activated, it will take care of building an Elasticsearch index. The search queries no longer hit the SQL database. Instead, the search query requests are sent to the cloud Elasticsearch server, more precisely to:

https://public-api.wordpress.com/rest/v1.3/sites/{your-blog-id}/search

There are a lot of search parameters to specify within the URL above. In our case, we will add the following:

  • filter[bool][must][0][term][post_type]=post: We only need results that are posts here, simply because our Gatsby website is limited to post. In real-life use, you might need spend some time configuring the boolean queries.
  • size=10 sets the number of returned results (maximum 20).
  • with highlight_fields[0]=title, we get the title string (or a part of it) with the searchTerm within the <mark> tags.
  • highlight_fields[0]=content is the same as below but for the post’s content.

There are three more search parameters depending on the user’s action:

  • query: The search term from the search input, e.g. gallery
  • sort: how the results should be orderer, the default is by score "score_default" (relevance) but there is also "date_asc" (newest) and "date_desc" (oldest)
  • page_handle: something like the “after” cursor for paginated results. We only request 10 results at once, and we will have a “load more” button.

Now, let’s see how a successful response is structured:

{
  total: 9,
  corrected_query: false,
  page_handle: false, // or a string it the total value > 10
  results: [
    {
      _score: 196.51814,
      fields: {
        date: '2018-11-03 03:55:09',
        'title.default': 'Block: Gallery',
        'excerpt.default': '',
        post_id: 1918,
        // we can configure what fields we want to add here with the query search parameters
      },
      result_type: 'post',
      railcar: {/* we will not use this data */},
      highlight: {
        title: ['Block: <mark>Gallery</mark>'],
        content: [
          'automatically stretch to the width of your <mark>gallery</mark>. ... A four column <mark>gallery</mark> with a wide width:',
          '<mark>Gallery</mark> blocks have two settings: the number of columns, and whether or not images should be cropped',
        ],
      },
    },
    /* more results */
  ],
  suggestions: [], // we will not use suggestions here
  aggregations: [], // nor the aggregations
}

The results field provides an array containing the database post IDs. To display the search results within a Gatsby site, we need to extract the corresponding post nodes (in particular their uri ) from the Gatsby data layer. My approach is to implement an instant search with asynchronous calls to the rest API and intersect the results with those of the static GraphQL query that returns all post nodes.

Let’s start by building an instant search widget that communicates with the search API. Since this is not specific to Gatsby, let’s see it in action in this Pen:

Here, useDebouncedInstantSearch is a custom hook responsible for fetching the results from the Jetpack Search API. My solution uses the awesome-debounce-promise library that allows us to take some extra care of the fetching mechanism. An instant search responds to the input directly without waiting for an explicit “Go!” from the user. If I’m typing fast, the request may change several times before even the first response arrives. Thus, there might be some unnecessary network bandwidth waste. The awesome-debounce-promise waits a given time interval (say 300ms) before making a call to an API; if there is a new call within this interval, the previous one will never be executed. It also resolves only the last promise returned from the call — this prevents the concurrency issues.

Now, with the search results available, let’s move back to Gatsby and build another custom hook:

import {useStaticQuery, graphql} from "gatsby"

export const useJetpackSearch = (params) => {
  const {
    allWpPost: { nodes },
  } = useStaticQuery(graphql`
    query AllPostsQuery {
      allWpPost {
        nodes {
          id
          databaseId
          uri
          title
          excerpt
        }
      }
    }
  `)
  const { error, loading, data } = useDebouncedInstantSearch(params)
  return {
    error,
    loading,
    data: {
      ...data,
      // map the results
      results: data.results.map(el => {
        // for each result find a node that has the same databaseId as the result field post_id
        const node = nodes.find(item => item.databaseId === el.fields.post_id)
        return {
          // spread the node
          ...node,
          // keep the highlight info
          highlight: el.highlight
        }
      }),
    }
  }
}

I will call the useJetpackSearch within <SearchResults />. The Gatsby-version of <SearchResults /> is almost identical as that in the Pen above. The differences are highlighted in the code block below. The hook useDebouncedInstantSearch is replaced by useJetpackSearch (that calls the former internally). There is a Gatsby Link that replaces h2 as well as el.fields["title.default"] and el.fields["excerpt.default"] are replaced by el.title and el.excerpt.

const SearchResults = ({ params, setParams }) => {
  const { loading, error, data } = useJetpackSearch(params)
  const { searchTerm } = params
  if (error) {
    return <p>Error - {error}</p>
  }
  return (
    <section className="search-results">
      {loading ? (
        <p className="info">Searching posts .....</p>
      ) : (
        <>
          {data.total !== undefined && (
            <p>
              Found {data.total} results for{" "}
              {data.corrected_query ? (
                <>
                  <del>{searchTerm}</del> <span>{data.corrected_query}</span>
                </>
              ) : (
                <span>{searchTerm}</span>
              )}
            </p>
          )}
        </>
      )}
      {data.results?.length > 0 && (
        <ul>
          {data.results.map((el) => {
            return (
              <li key={el.id}>
                <Link to={el.uri}>
                  {el.highlight.title[0]
                    ? el.highlight.title.map((item, index) => (
                        <React.Fragment key={index}>
                          {parse(item)}
                        </React.Fragment>
                      ))
                    : parse(el.title)}
                </Link>
                <div className="post-excerpt">
                  {el.highlight.content[0]
                    ? el.highlight.content.map((item, index) => (
                        <div key={index}>{parse(item)}</div>
                      ))
                    : parse(el.excerpt)}
                </div>
              </li>
            );
          })}
        </ul>
      )}
      {data.page_handle && (
        <button
          type="button"
          disabled={loading}
          onClick={() => setParams({ pageHandle: data.page_handle })}
        >
          {loading ? "loading..." : "load more"}
        </button>
      )}
    </section>
  )
}

You can find the complete code in this repo and see it in action in this demo. Note that I no longer source WordPress data from the generic WordPress demo used by Gatsby starter. I need to have a website with Jetpack Search activated.

Wrapping up

We’ve just seen two ways of dealing with search in headless WordPress. Besides a few Gatsby-specific technical details (like using Gatsby Browser API), you can implement both discussed approaches within other frameworks. We’ve seen how to make use of the native WordPress search. I guess that it is an acceptable solution in many cases.

But if you need something better, there are better options available. One of them is Jetpack Search. Jetpack Instant Search does a great job on CSS-Tricks and, as we’ve just seen, can work with headless WordPress as well. There are probably other ways of implementing it. You can also go further with the query configuration, the filter functionalities, and how you display the results.


The post Native Search vs. Jetpack Instant Search in Headless WordPress With Gatsby appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

How to Add Lunr Search to your Gatsby Website

The Jamstack way of thinking and building websites is becoming more and more popular.

Have you already tried Gatsby, Nuxt, or Gridsome (to cite only a few)? Chances are that your first contact was a “Wow!” moment — so many things are automatically set up and ready to use. 

There are some challenges, though, one of which is search functionality. If you’re working on any sort of content-driven site, you’ll likely run into search and how to handle it. Can it be done without any external server-side technology? 

Search is not one of those things that come out of the box with Jamstack. Some extra decisions and implementation are required.

Fortunately, we have a bunch of options that might be more or less adapted to a project. We could use Algolia’s powerful search-as-service API. It comes with a free plan that is restricted to non-commercial projects with  a limited capacity. If we were to use WordPress with WPGraphQL as a data source, we could take advantage of WordPress native search functionality and Apollo Client. Raymond Camden recently explored a few Jamstack search options, including pointing a search form directly at Google.

In this article, we will build a search index and add search functionality to a Gatsby website with Lunr, a lightweight JavaScript library providing an extensible and customizable search without the need for external, server-side services. We used it recently to add “Search by Tartan Name” to our Gatsby project tartanify.com. We absolutely wanted persistent search as-you-type functionality, which brought some extra challenges. But that’s what makes it interesting, right? I’ll discuss some of the difficulties we faced and how we dealt with them in the second half of this article.

Getting started

For the sake of simplicity, let’s use the official Gatsby blog starter. Using a generic starter lets us abstract many aspects of building a static website. If you’re following along, make sure to install and run it:

gatsby new gatsby-starter-blog https://github.com/gatsbyjs/gatsby-starter-blog
cd gatsby-starter-blog
gatsby develop

It’s a tiny blog with three posts we can view by opening up http://localhost:8000/___graphql in the browser.

Showing the GraphQL page on the localhost installation in the browser.

Inverting index with Lunr.js 🙃

Lunr uses a record-level inverted index as its data structure. The inverted index stores the mapping for each word found within a website to its location (basically a set of page paths). It’s on us to decide which fields (e.g. title, content, description, etc.) provide the keys (words) for the index.

For our blog example, I decided to include all titles and the content of each article. Dealing with titles is straightforward since they are composed uniquely of words. Indexing content is a little more complex. My first try was to use the rawMarkdownBody field. Unfortunately, rawMarkdownBody introduces some unwanted keys resulting from the markdown syntax.

Showing an attempt at using markdown syntax for links.

I obtained a “clean” index using the html field in conjunction with the striptags package (which, as the name suggests, strips out the HTML tags). Before we get into the details, let’s look into the Lunr documentation.

Here’s how we create and populate the Lunr index. We will use this snippet in a moment, specifically in our gatsby-node.js file.

const index = lunr(function () {
  this.ref('slug')
  this.field('title')
  this.field('content')
  for (const doc of documents) {
    this.add(doc)
  }
})

 documents is an array of objects, each with a slug, title and content property:

{
  slug: '/post-slug/',
  title: 'Post Title',
  content: 'Post content with all HTML tags stripped out.'
}

We will define a unique document key (the slug) and two fields (the title and content, or the key providers). Finally, we will add all of the documents, one by one.

Let’s get started.

Creating an index in gatsby-node.js 

Let’s start by installing the libraries that we are going to use.

yarn add lunr graphql-type-json striptags

Next, we need to edit the gatsby-node.js file. The code from this file runs once in the process of building a site, and our aim is to add index creation to the tasks that Gatsby executes on build. 

CreateResolvers is one of the Gatsby APIs controlling the GraphQL data layer. In this particular case, we will use it to create a new root field; Let’s call it LunrIndex

Gatsby’s internal data store and query capabilities are exposed to GraphQL field resolvers on context.nodeModel. With getAllNodes, we can get all nodes of a specified type:

/* gatsby-node.js */
const { GraphQLJSONObject } = require(`graphql-type-json`)
const striptags = require(`striptags`)
const lunr = require(`lunr`)

exports.createResolvers = ({ cache, createResolvers }) => {
  createResolvers({
    Query: {
      LunrIndex: {
        type: GraphQLJSONObject,
        resolve: (source, args, context, info) => {
          const blogNodes = context.nodeModel.getAllNodes({
            type: `MarkdownRemark`,
          })
          const type = info.schema.getType(`MarkdownRemark`)
          return createIndex(blogNodes, type, cache)
        },
      },
    },
  })
}

Now let’s focus on the createIndex function. That’s where we will use the Lunr snippet we mentioned in the last section. 

/* gatsby-node.js */
const createIndex = async (blogNodes, type, cache) => {
  const documents = []
  // Iterate over all posts 
  for (const node of blogNodes) {
    const html = await type.getFields().html.resolve(node)
    // Once html is resolved, add a slug-title-content object to the documents array
    documents.push({
      slug: node.fields.slug,
      title: node.frontmatter.title,
      content: striptags(html),
    })
  }
  const index = lunr(function() {
    this.ref(`slug`)
    this.field(`title`)
    this.field(`content`)
    for (const doc of documents) {
      this.add(doc)
    }
  })
  return index.toJSON()
}

Have you noticed that instead of accessing the HTML element directly with  const html = node.html, we’re using an  await expression? That’s because node.html isn’t available yet. The gatsby-transformer-remark plugin (used by our starter to parse Markdown files) does not generate HTML from markdown immediately when creating the MarkdownRemark nodes. Instead,  html is generated lazily when the html field resolver is called in a query. The same actually applies to the excerpt that we will need in just a bit.

Let’s look ahead and think about how we are going to display search results. Users expect to obtain a link to the matching post, with its title as the anchor text. Very likely, they wouldn’t mind a short excerpt as well.

Lunr’s search returns an array of objects representing matching documents by the ref property (which is the unique document key slug in our example). This array does not contain the document title nor the content. Therefore, we need to store somewhere the post title and excerpt corresponding to each slug. We can do that within our LunrIndex as below:

/* gatsby-node.js */
const createIndex = async (blogNodes, type, cache) => {
  const documents = []
  const store = {}
  for (const node of blogNodes) {
    const {slug} = node.fields
    const title = node.frontmatter.title
    const [html, excerpt] = await Promise.all([
      type.getFields().html.resolve(node),
      type.getFields().excerpt.resolve(node, { pruneLength: 40 }),
    ])
    documents.push({
      // unchanged
    })
    store[slug] = {
      title,
      excerpt,
    }
  }
  const index = lunr(function() {
    // unchanged
  })
  return { index: index.toJSON(), store }
}

Our search index changes only if one of the posts is modified or a new post is added. We don’t need to rebuild the index each time we run gatsby develop. To avoid unnecessary builds, let’s take advantage of the cache API:

/* gatsby-node.js */
const createIndex = async (blogNodes, type, cache) => {
  const cacheKey = `IndexLunr`
  const cached = await cache.get(cacheKey)
  if (cached) {
    return cached
  }
  // unchanged
  const json = { index: index.toJSON(), store }
  await cache.set(cacheKey, json)
  return json
}

Enhancing pages with the search form component

We can now move on to the front end of our implementation. Let’s start by building a search form component.

touch src/components/search-form.js 

I opt for a straightforward solution: an input of type="search", coupled with a label and accompanied by a submit button, all wrapped within a form tag with the search landmark role.

We will add two event handlers, handleSubmit on form submit and handleChange on changes to the search input.

/* src/components/search-form.js */
import React, { useState, useRef } from "react"
import { navigate } from "@reach/router"
const SearchForm = ({ initialQuery = "" }) => {
  // Create a piece of state, and initialize it to initialQuery
  // query will hold the current value of the state,
  // and setQuery will let us change it
  const [query, setQuery] = useState(initialQuery)
  
  // We need to get reference to the search input element
  const inputEl = useRef(null)

  // On input change use the current value of the input field (e.target.value)
  // to update the state's query value
  const handleChange = e => {
    setQuery(e.target.value)
  }
  
  // When the form is submitted navigate to /search
  // with a query q paramenter equal to the value within the input search
  const handleSubmit = e => {
    e.preventDefault()
    // `inputEl.current` points to the mounted search input element
    const q = inputEl.current.value
    navigate(`/search?q=${q}`)
  }
  return (
    <form role="search" onSubmit={handleSubmit}>
      <label htmlFor="search-input" style={{ display: "block" }}>
        Search for:
      </label>
      <input
        ref={inputEl}
        id="search-input"
        type="search"
        value={query}
        placeholder="e.g. duck"
        onChange={handleChange}
      />
      <button type="submit">Go</button>
    </form>
  )
}
export default SearchForm

Have you noticed that we’re importing navigate from the @reach/router package? That is necessary since neither Gatsby’s <Link/> nor navigate provide in-route navigation with a query parameter. Instead, we can import @reach/router — there’s no need to install it since Gatsby already includes it — and use its navigate function.

Now that we’ve built our component, let’s add it to our home page (as below) and 404 page.

/* src/pages/index.js */
// unchanged
import SearchForm from "../components/search-form"
const BlogIndex = ({ data, location }) => {
  // unchanged
  return (
    <Layout location={location} title={siteTitle}>
      <SEO title="All posts" />
      <Bio />
      <SearchForm />
      // unchanged

Search results page

Our SearchForm component navigates to the /search route when the form is submitted, but for the moment, there is nothing behing this URL. That means we need to add a new page:

touch src/pages/search.js 

I proceeded by copying and adapting the content of the the index.js page. One of the essential modifications concerns the page query (see the very bottom of the file). We will replace allMarkdownRemark with the LunrIndex field. 

/* src/pages/search.js */
import React from "react"
import { Link, graphql } from "gatsby"
import { Index } from "lunr"
import Layout from "../components/layout"
import SEO from "../components/seo"
import SearchForm from "../components/search-form"


// We can access the results of the page GraphQL query via the data props
const SearchPage = ({ data, location }) => {
  const siteTitle = data.site.siteMetadata.title
  
  // We can read what follows the ?q= here
  // URLSearchParams provides a native way to get URL params
  // location.search.slice(1) gets rid of the "?" 
  const params = new URLSearchParams(location.search.slice(1))
  const q = params.get("q") || ""


  // LunrIndex is available via page query
  const { store } = data.LunrIndex
  // Lunr in action here
  const index = Index.load(data.LunrIndex.index)
  let results = []
  try {
    // Search is a lunr method
    results = index.search(q).map(({ ref }) => {
      // Map search results to an array of {slug, title, excerpt} objects
      return {
        slug: ref,
        ...store[ref],
      }
    })
  } catch (error) {
    console.log(error)
  }
  return (
    // We will take care of this part in a moment
  )
}
export default SearchPage
export const pageQuery = graphql`
  query {
    site {
      siteMetadata {
        title
      }
    }
    LunrIndex
  }
`

Now that we know how to retrieve the query value and the matching posts, let’s display the content of the page. Notice that on the search page we pass the query value to the <SearchForm /> component via the initialQuery props. When the user arrives to the search results page, their search query should remain in the input field. 

return (
  <Layout location={location} title={siteTitle}>
    <SEO title="Search results" />
    {q ? <h1>Search results</h1> : <h1>What are you looking for?</h1>}
    <SearchForm initialQuery={q} />
    {results.length ? (
      results.map(result => {
        return (
          <article key={result.slug}>
            <h2>
              <Link to={result.slug}>
                {result.title || result.slug}
              </Link>
            </h2>
            <p>{result.excerpt}</p>
          </article>
        )
      })
    ) : (
      <p>Nothing found.</p>
    )}
  </Layout>
)

You can find the complete code in this gatsby-starter-blog fork and the live demo deployed on Netlify.

Instant search widget

Finding the most “logical” and user-friendly way of implementing search may be a challenge in and of itself. Let’s now switch to the real-life example of tartanify.com — a Gatsby-powered website gathering 5,000+ tartan patterns. Since tartans are often associated with clans or organizations, the possibility to search a tartan by name seems to make sense. 

We built tartanify.com as a side project where we feel absolutely free to experiment with things. We didn’t want a classic search results page but an instant search “widget.” Often, a given search keyword corresponds with a number of results — for example, “Ramsay” comes in six variations.  We imagined the search widget would be persistent, meaning it should stay in place when a user navigates from one matching tartan to another.

Let me show you how we made it work with Lunr.  The first step of building the index is very similar to the gatsby-starter-blog example, only simpler:

/* gatsby-node.js */
exports.createResolvers = ({ cache, createResolvers }) => {
  createResolvers({
    Query: {
      LunrIndex: {
        type: GraphQLJSONObject,
        resolve(source, args, context) {
          const siteNodes = context.nodeModel.getAllNodes({
            type: `TartansCsv`,
          })
          return createIndex(siteNodes, cache)
        },
      },
    },
  })
}
const createIndex = async (nodes, cache) => {
  const cacheKey = `LunrIndex`
  const cached = await cache.get(cacheKey)
  if (cached) {
    return cached
  }
  const store = {}
  const index = lunr(function() {
    this.ref(`slug`)
    this.field(`title`)
    for (node of nodes) {
      const { slug } = node.fields
      const doc = {
        slug,
        title: node.fields.Unique_Name,
      }
      store[slug] = {
        title: doc.title,
      }
      this.add(doc)
    }
  })
  const json = { index: index.toJSON(), store }
  cache.set(cacheKey, json)
  return json
}

We opted for instant search, which means that search is triggered by any change in the search input instead of a form submission.

/* src/components/searchwidget.js */
import React, { useState } from "react"
import lunr, { Index } from "lunr"
import { graphql, useStaticQuery } from "gatsby"
import SearchResults from "./searchresults"


const SearchWidget = () => {
  const [value, setValue] = useState("")
  // results is now a state variable 
  const [results, setResults] = useState([])


  // Since it's not a page component, useStaticQuery for quering data
  // https://www.gatsbyjs.org/docs/use-static-query/
  const { LunrIndex } = useStaticQuery(graphql`
    query {
      LunrIndex
    }
  `)
  const index = Index.load(LunrIndex.index)
  const { store } = LunrIndex
  const handleChange = e => {
    const query = e.target.value
    setValue(query)
    try {
      const search = index.search(query).map(({ ref }) => {
        return {
          slug: ref,
          ...store[ref],
        }
      })
      setResults(search)
    } catch (error) {
      console.log(error)
    }
  }
  return (
    <div className="search-wrapper">
      // You can use a form tag as well, as long as we prevent the default submit behavior
      <div role="search">
        <label htmlFor="search-input" className="visually-hidden">
          Search Tartans by Name
        </label>
        <input
          id="search-input"
          type="search"
          value={value}
          onChange={handleChange}
          placeholder="Search Tartans by Name"
        />
      </div>
      <SearchResults results={results} />
    </div>
  )
}
export default SearchWidget

The SearchResults are structured like this:

/* src/components/searchresults.js */
import React from "react"
import { Link } from "gatsby"
const SearchResults = ({ results }) => (
  <div>
    {results.length ? (
      <>
        <h2>{results.length} tartan(s) matched your query</h2>
        <ul>
          {results.map(result => (
            <li key={result.slug}>
              <Link to={`/tartan/${result.slug}`}>{result.title}</Link>
            </li>
          ))}
        </ul>
      </>
    ) : (
      <p>Sorry, no matches found.</p>
    )}
  </div>
)
export default SearchResults

Making it persistent

Where should we use this component? We could add it to the Layout component. The problem is that our search form will get unmounted on page changes that way. If a user wants to browser all tartans associated with the “Ramsay” clan, they will have to retype their query several times. That’s not ideal.

Thomas Weibenfalk has written a great article on keeping state between pages with local state in Gatsby.js. We will use the same technique, where the wrapPageElement browser API sets persistent UI elements around pages. 

Let’s add the following code to the gatsby-browser.js. You might need to add this file to the root of your project.

/* gatsby-browser.js */
import React from "react"
import SearchWrapper from "./src/components/searchwrapper"
export const wrapPageElement = ({ element, props }) => (
  <SearchWrapper {...props}>{element}</SearchWrapper>
)

Now let’s add a new component file:

touch src/components/searchwrapper.js

Instead of adding SearchWidget component to the Layout, we will add it to the SearchWrapper and the magic happens. ✨

/* src/components/searchwrapper.js */
import React from "react"
import SearchWidget from "./searchwidget"


const SearchWrapper = ({ children }) => (
  <>
    {children}
    <SearchWidget />
  </>
)
export default SearchWrapper

Creating a custom search query

At this point, I started to try different keywords but very quickly realized that Lunr’s default search query might not be the best solution when used for instant search.

Why? Imagine that we are looking for tartans associated with the name MacCallum. While typing “MacCallum” letter-by-letter, this is the evolution of the results:

  • m – 2 matches (Lyon, Jeffrey M, Lyon, Jeffrey M (Hunting))
  • ma – no matches
  • mac – 1 match (Brighton Mac Dermotte)
  • macc – no matches
  • macca – no matches
  • maccal – 1 match (MacCall)
  • maccall – 1 match (MacCall)
  • maccallu – no matches
  • maccallum – 3 matches (MacCallum, MacCallum #2, MacCallum of Berwick)

Users will probably type the full name and hit the button if we make a button available. But with instant search, a user is likely to abandon early because they may expect that the results can only narrow down letters are added to the keyword query.

 That’s not the only problem. Here’s what we get with “Callum”:

  • c – 3 unrelated matches
  • ca – no matches
  • cal – no matches
  • call – no matches
  • callu – no matches
  • callum – one match 

You can see the trouble if someone gives up halfway into typing the full query.

Fortunately, Lunr supports more complex queries, including fuzzy matches, wildcards and boolean logic (e.g. AND, OR, NOT) for multiple terms. All of these are available either via a special query syntax, for example: 

index.search("+*callum mac*")

We could also reach for the index query method to handle it programatically.

The first solution is not satisfying since it requires more effort from the user. I used the index.query method instead:

/* src/components/searchwidget.js */
const search = index
  .query(function(q) {
    // full term matching
    q.term(el)
    // OR (default)
    // trailing or leading wildcard
    q.term(el, {
      wildcard:
        lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING,
    })
  })
  .map(({ ref }) => {
    return {
      slug: ref,
      ...store[ref],
    }
  })

Why use full term matching with wildcard matching? That’s necessary for all keywords that “benefit” from the stemming process. For example, the stem of “different” is “differ.”  As a consequence, queries with wildcards — such as differe*, differen* or  different* — all result in no matches, while the full term queries differe, differen and different return matches.

Fuzzy matches can be used as well. In our case, they are allowed uniquely for terms of five or more characters:

q.term(el, { editDistance: el.length > 5 ? 1 : 0 })
q.term(el, {
  wildcard:
    lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING,
})

The handleChange function also “cleans up” user inputs and ignores single-character terms:

/* src/components/searchwidget.js */  
const handleChange = e => {
  const query = e.target.value || ""
  setValue(query)
  if (!query.length) {
    setResults([])
  }
  const keywords = query
    .trim() // remove trailing and leading spaces
    .replace(/\*/g, "") // remove user's wildcards
    .toLowerCase()
    .split(/\s+/) // split by whitespaces
  // do nothing if the last typed keyword is shorter than 2
  if (keywords[keywords.length - 1].length < 2) {
    return
  }
  try {
    const search = index
      .query(function(q) {
        keywords
          // filter out keywords shorter than 2
          .filter(el => el.length > 1)
          // loop over keywords
          .forEach(el => {
            q.term(el, { editDistance: el.length > 5 ? 1 : 0 })
            q.term(el, {
              wildcard:
                lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING,
            })
          })
      })
      .map(({ ref }) => {
        return {
          slug: ref,
          ...store[ref],
        }
      })
    setResults(search)
  } catch (error) {
    console.log(error)
  }
}

Let’s check it in action:

  • m – pending
  • ma – 861 matches
  • mac – 600 matches
  • macc – 35 matches
  • macca – 12 matches
  • maccal – 9 matches
  • maccall – 9 matches
  • maccallu – 3 matches
  • maccallum – 3 matches

Searching for “Callum” works as well, resulting in four matches: Callum, MacCallum, MacCallum #2, and MacCallum of Berwick.

There is one more problem, though: multi-terms queries. Say, you’re looking for “Loch Ness.” There are two tartans associated with  that term, but with the default OR logic, you get a grand total of 96 results. (There are plenty of other lakes in Scotland.)

I wound up deciding that an AND search would work better for this project. Unfortunately, Lunr does not support nested queries, and what we actually need is (keyword1 OR *keyword*) AND (keyword2 OR *keyword2*). 

To overcome this, I ended up moving the terms loop outside the query method and intersecting the results per term. (By intersecting, I mean finding all slugs that appear in all of the per-single-keyword results.)

/* src/components/searchwidget.js */
try {
  // andSearch stores the intersection of all per-term results
  let andSearch = []
  keywords
    .filter(el => el.length > 1)
    // loop over keywords
    .forEach((el, i) => {
      // per-single-keyword results
      const keywordSearch = index
        .query(function(q) {
          q.term(el, { editDistance: el.length > 5 ? 1 : 0 })
          q.term(el, {
            wildcard:
              lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING,
          })
        })
        .map(({ ref }) => {
          return {
            slug: ref,
            ...store[ref],
          }
        })
      // intersect current keywordSearch with andSearch
      andSearch =
        i > 0
          ? andSearch.filter(x => keywordSearch.some(el => el.slug === x.slug))
          : keywordSearch
    })
  setResults(andSearch)
} catch (error) {
  console.log(error)
}

The source code for tartanify.com is published on GitHub. You can see the complete implementation of the Lunr search there.

Final thoughts

Search is often a non-negotiable feature for finding content on a site. How important the search functionality actually is may vary from one project to another. Nevertheless, there is no reason to abandon it under the pretext that it does not tally with the static character of Jamstack websites. There are many possibilities. We’ve just discussed one of them.

And, paradoxically in this specific example, the result was a better all-around user experience, thanks to the fact that implementing search was not an obvious task but instead required a lot of deliberation. We may not have been able to say the same with an over-the-counter solution.

The post How to Add Lunr Search to your Gatsby Website appeared first on CSS-Tricks.

How We Created a Static Site That Generates Tartan Patterns in SVG

Tartan is a patterned cloth that’s typically associated with Scotland, particularly their fashionable kilts. On tartanify.com, we gathered over 5,000 tartan patterns (as SVG and PNG files), taking care to filter out any that have explicit usage restrictions.

The idea was cooked up by Sylvain Guizard during our summer holidays in Scotland. At the very beginning, we were thinking of building the pattern library manually in some graphics software, like Adobe Illustrator or Sketch. But that was before we discovered that the number of tartan patterns comes in thousands. We felt overwhelmed and gave up… until I found out that tartans have a specific anatomy and are referenced by simple strings composed of the numbers of threads and color codes.

Tartan anatomy and SVG

Tartan is made with alternating bands of colored threads woven at right angles that are parallel to each other. The vertical and horizontal bands follow the same pattern of colors and widths. The rectangular areas where the horizontal and vertical bands cross give the appearance of new colors by blending the original ones. Moreover, tartans are woven with a specific technique called twill, which results in visible diagonal lines. I tried to recreate the technique with SVG rectangles as threads here:

Let’s analyze the following SVG structure:


<svg viewBox="0 0 280 280" width="280" height="280" x="0"  y="0" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <mask id="grating" x="0" y="0" width="1" height="1">
      <rect x="0" y="0" width="100%" height="100%" fill="url(#diagonalStripes)"/>
    </mask>
  </defs>
  <g id="horizontalStripes">
    <rect fill="#FF8A00" height="40" width="100%" x="0" y="0"/>    
    <rect fill="#E52E71" height="10" width="100%" x="0" y="40"/>
    <rect fill="#FFFFFF" height="10" width="100%" x="0" y="50"/>
    <rect fill="#E52E71" height="70" width="100%" x="0" y="60"/>   
    <rect fill="#100E17" height="20" width="100%" x="0" y="130"/>    
    <rect fill="#E52E71" height="70" width="100%" x="0" y="150"/>
    <rect fill="#FFFFFF" height="10" width="100%" x="0" y="220"/>
    <rect fill="#E52E71" height="10" width="100%" x="0" y="230"/>   
    <rect fill="#FF8A00" height="40" width="100%" x="0" y="240"/>
  </g>
  <g id="verticalStripes" mask="url(#grating)">
    <rect fill="#FF8A00" width="40" height="100%" x="0" y="0" />  
    <rect fill="#E52E71" width="10" height="100%" x="40" y="0" />
    <rect fill="#FFFFFF" width="10" height="100%" x="50" y="0" />
    <rect fill="#E52E71" width="70" height="100%" x="60" y="0" />
    <rect fill="#100E17" width="20" height="100%" x="130" y="0" />   
    <rect fill="#E52E71" width="70" height="100%" x="150" y="0" />
    <rect fill="#FFFFFF" width="10" height="100%" x="220" y="0" />
    <rect fill="#E52E71" width="10" height="100%" x="230" y="0" />   
    <rect fill="#FF8A00" width="40" height="100%" x="240" y="0" />
  </g>
</svg>

The horizontalStripes group creates a 280x280 square with horizontal stripes. The verticalStripes group creates the same square, but rotated by 90 degrees. Both squares start at (0,0) coordinates. That means the horizontalStripes are completely covered by the verticalStripes; that is, unless we apply a mask on the upper one.

<defs>
  <mask id="grating" x="0" y="0" width="1" height="1">
    <rect x="0" y="0" width="100%" height="100%" fill="url(#diagonalStripes)"/>
  </mask>
</defs>

The mask SVG element defines an alpha mask. By default, the coordinate system used for its x, y, width, and height attributes is the objectBoundingBox. Setting width and height to 1 (or 100%) means that the mask covers the verticalStripes resulting in just the white parts within the mask being full visible.

Can we fill our mask with a pattern? Yes, we can! Let’s reflect the tartan weaving technique using a pattern tile, like this:

In the pattern definition we change the patternUnits from the default  objectBoundingBox to userSpaceOnUse so that now, width and height are defined in pixels.

<svg width="0" height="0">
  <defs>
    <pattern id="diagonalStripes" x="0" y="0" patternUnits="userSpaceOnUse" width="8" height="8">
      <polygon points="0,4 0,8 8,0 4,0" fill="white"/>
      <polygon points="4,8 8,8 8,4" fill="white"/>
    </pattern>    
  </defs> 
</svg>

Using React for tartan weaving

We just saw how we can create a manual “weave” with SVG. Now let’s automatize this process with React. 

The SvgDefs component is straightforward — it returns the defs markup.

const SvgDefs = () => {
  return (
    <defs>
      <pattern
        id="diagonalStripes"
        x="0"
        y="0"
        width="8"
        height="8"
        patternUnits="userSpaceOnUse"
      >
        <polygon points="0,4 0,8 8,0 4,0" fill="#ffffff" />
        <polygon points="4,8 8,8 8,4" fill="#ffffff" />
      </pattern>
      <mask id="grating" x="0" y="0" width="1" height="1">
        <rect
          x="0"
          y="0"
          width="100%"
          height="100%"
          fill="url(#diagonalStripes)"
        />
      </mask>
    </defs>
  )
}

We will represent a tartan as an array of stripes. Each stripe is an object with two properties: fill (a hex color) and size (a number).

const tartan = [
  { fill: "#FF8A00", size: 40 },
  { fill: "#E52E71", size: 10 },
  { fill: "#FFFFFF", size: 10 },
  { fill: "#E52E71", size: 70 },
  { fill: "#100E17", size: 20 },
  { fill: "#E52E71", size: 70 },
  { fill: "#FFFFFF", size: 10 },
  { fill: "#E52E71", size: 10 },
  { fill: "#FF8A00", size: 40 },
]

Tartans data is often available as a pair of strings: Palette and Threadcount that could look like this:

// Palette
O#FF8A00 P#E52E71 W#FFFFFF K#100E17

// Threadcount
O/40 P10 W10 P70 K/10.

I won’t cover how to convert this string representation into the stripes array but, if you are interested, you can find my method in this Gist.

The SvgTile component takes the tartan array as props and returns an SVG structure.

const SvgTile = ({ tartan }) => {

  // We need to calculate the starting position of each stripe and the total size of the tile
  const cumulativeSizes = tartan
    .map(el => el.size)
    .reduce(function(r, a) {
      if (r.length > 0) a += r[r.length - 1]
      r.push(a)
      return r
    }, [])
  
  // The tile size
  const size = cumulativeSizes[cumulativeSizes.length - 1]

  return (
    <svg
      viewBox={`0 0 ${size} ${size}`}
      width={size}
      height={size}
      x="0"
      y="0"
      xmlns="http://www.w3.org/2000/svg"
    >
      <SvgDefs />
      <g id="horizontalStripes">
        {tartan.map((el, index) => {
          return (
            <rect
              fill={el.fill}
              width="100%"
              height={el.size}
              x="0"
              y={cumulativeSizes[index - 1] || 0}
            />
          )
        })}
      </g>
      <g id="verticalStripes" mask="url(#grating)">
        {tartan.map((el, index) => {
          return (
            <rect
              fill={el.fill}
              width={el.size}
              height="100%"
              x={cumulativeSizes[index - 1] || 0}
              y="0"
            />
          )
        })}
      </g>
    </svg>
  )
}

Using a tartan SVG tile as a background image

On tartanify.com, each individual tartan is used as a background image on a full-screen element. This requires some extra manipulation since we don’t have our tartan pattern tile as an SVG image. We're also unable to use an inline SVG directly in the background-image property.

Fortunately, encoding the SVG as a background image does work:

.bg-element {
  background-image: url('data:image/svg+xml;charset=utf-8,<svg>...</svg>');
}

Let’s now create an SvgBg component. It takes the tartan array as props and returns a full-screen div with the tartan pattern as background.

We need to convert the SvgTile React object into a string. The ReactDOMServer object allows us to render components to static markup. Its method renderToStaticMarkup is available both in the browser and on the Node server. The latter is important since later we will server render the tartan pages with Gatsby.

const tartanStr = ReactDOMServer.renderToStaticMarkup(<SvgTile tartan={tartan} />)

Our SVG string contains hex color codes starting with the # symbol. At the same time, # starts a fragment identifier in a URL. It means our code will break unless we escape all of those instances. That’s where the built-in JavaScript encodeURIComponent function comes in handy.

const SvgBg = ({ tartan }) => {
  const tartanStr = ReactDOMServer.renderToStaticMarkup(<SvgTile tartan={tartan} />)
  const tartanData = encodeURIComponent(tartanStr)
  return (
    <div
      style={{
        width: "100%",
        height: "100vh",
        backgroundImage: `url("data:image/svg+xml;utf8,${tartanData}")`,
      }}
    />
  )
}

Making an SVG tartan tile downloadable

Let’s now download our SVG image.

The SvgDownloadLink component takes svgData (the already encoded SVG string) and fileName as props and creates an anchor (<a>) element. The download attribute prompts the user to save the linked URL instead of navigating to it. When used with a value, it suggests the name of the destination file.

const SvgDownloadLink = ({ svgData, fileName = "file" }) => {
  return (
    <a
      download={`${fileName}.svg`}
      href={`data:image/svg+xml;utf8,${svgData}`}
    >
      Download as SVG
    </a>
  )
}

Converting an SVG tartan tile to a high-res PNG image file

What about users that prefer the PNG image format over SVG? Can we provide them with high resolution PNGs?

The PngDownloadLink component, just like SvgDownloadLink, creates an anchor tag and has the tartanData and fileName as props. In this case however, we also need to provide the tartan tile size since we need to set the canvas dimensions.

const Tile = SvgTile({tartan})
// Tartan tiles are always square
const tartanSize = Tile.props.width

In the browser, once the component is ready, we draw the SVG tile on a <canvas> element. We’ll use the canvas toDataUrl() method that returns the image as a data URI. Finally, we set the date URI as the href attribute of our anchor tag.

Notice that we use double dimensions for the canvas and double scale the ctx. This way, we will output a PNG that’s double the size, which is great for high-resolution usage.

const PngDownloadLink = ({ svgData, width, height, fileName = "file" }) => {
  const aEl = React.createRef()
  React.useEffect(() => {
    const canvas = document.createElement("canvas")
    canvas.width = 2 * width
    canvas.height = 2 * height
    const ctx = canvas.getContext("2d")
    ctx.scale(2, 2)
    let img = new Image()
    img.src = `data:image/svg+xml, ${svgData}`
    img.onload = () => {
      ctx.drawImage(img, 0, 0)
      const href = canvas.toDataURL("image/png")
      aEl.current.setAttribute("href", href)
    }
  }, [])
  return (
    <a 
      ref={aEl} 
      download={`${fileName}.png`}
    >
      Download as PNG
    </a>
  )
}

For that demo, I could have skipped React's useEffect hook and the code would worked fine. Nevertheless, our code is executed both on the server and in the browser, thanks to Gatsby. Before we start creating the canvas, we need to be sure that we are in a browser. We should also make sure the anchor element is ”ready” before we modify its attribute. 

Making a static website out of CSV with Gatsby

If you haven’t already heard of Gatsby, it’s a free and open source framework that allows you to pull data from almost anywhere and generate static websites that are powered by React.

Tartanify.com is a Gatsby website coded by myself and designed by Sylvain. At the beginning of the project, all we had was a huge CSV file (seriously, 5,495 rows), a method to convert the palette and threadcount strings into the tartan SVG structure, and an objective to give Gatsby a try.

In order to use a CSV file as the data source, we need two Gatsby plugins: gatsby-transformer-csv and gatsby-source-filesystem. Under the hood, the source plugin reads the files in the /src/data folder (which is where we put the tartans.csv file), then the transformer plugin parses the CSV file into JSON arrays.

// gatsby-config.js
module.exports = {
  /* ... */
  plugins: [
    'gatsby-transformer-csv',
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/data`,
        name: 'data',
      },
    },
  ],
}

Now, let’s see what happens in the gatsby-node.js file. The file is run during the site-building process. That’s where we can use two Gatsby Node APIs: createPages and onCreateNode. onCreateNode is called when a new node is created. We will add two additional fields to a tartan node: its unique slug and a unique name. It is necessary since the CSV file contains a number of tartan variants that are stored under the same name.

// gatsby-node.js
// We add slugs here and use this array to check if a slug is already in use
let slugs = []
// Then, if needed, we append a number
let i = 1

exports.onCreateNode = ({ node, actions }) => {
  if (node.internal.type === 'TartansCsv') {
    // This transforms any string into slug
    let slug = slugify(node.Name)
    let uniqueName = node.Name
    // If the slug is already in use, we will attach a number to it and the uniqueName
    if (slugs.indexOf(slug) !== -1) {
      slug += `-${i}`
      uniqueName += ` ${i}`
      i++
    } else {
      i = 1
    }
    slugs.push(slug)
  
    // Adding fields to the node happen here
    actions.createNodeField({
      name: 'slug',
      node,
      value: slug,
    })
    actions.createNodeField({
      name: 'Unique_Name',
      node,
      value: uniqueName,
    })
  }
}

Next, we create pages for each individual tartan. We want to have access to its siblings so that we can navigate easily. We will query the previous and next edges and add the result to the tartan page context.

// gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  const allTartans = await graphql(`
    query {
      allTartansCsv {
        edges {
          node {
            id
            fields {
              slug
            }
          }
          previous {
            fields {
              slug
              Unique_Name
            }
          }
          next {
            fields {
              slug
              Unique_Name
            }
          }
        }
      }
    }
  `)
  if (allTartans.errors) {
    throw allTartans.errors
  }
  allTartans.data.allTartansCsv.edges.forEach(
    ({ node, next, previous }) => {
      createPage({
        path: `/tartan/${node.fields.slug}`,
        component: path.resolve(`./src/templates/tartan.js`),
        context: {
          id: node.id,
          previous,
          next,
        },
      })
    }
  )
}

We decided to index tartans by letters and create paginated letter pages. These pages list tartans with links to their individual pages. We display a maximum of 60 tartans per page, and the number of pages per letter varies. For example, the letter “a” will have have four pages: tartans/a, tartans/a/2, tartans/a/3 and tartans/a/4. The highest number of pages (15) belongs to “m” due to a high number of traditional names starting with “Mac.”

The tartans/a/4 page should point to tartans/b as its next page and tartans/b should point to tartans/a/4 as its previous page.

We will run a for of loop through the letters array ["a", "b", ... , "z"] and query all tartans that start with a given letter. This can be done with filter and regex operator:

allTartansCsv(filter: { Name: { regex: "/^${letter}/i" } })

The previousLetterLastIndex variable will be updated at the end of each loop and store the number of pages per letter. The /tartans/b page need to know the number of a pages (4) since its previous link should be tartans/a/4.

// gatsby-node.js
const letters = "abcdefghijklmnopqrstuvwxyz".split("")
exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions
  // etc.

  let previousLetterLastIndex = 1
  for (const letter of letters) {
    const allTartansByLetter = await graphql(`
      query {
        allTartansCsv(filter: {Name: {regex: "/^${letter}/i"}}) {
          nodes {
            Palette
            fields {
              slug
              Unique_Name
            }
          }
          totalCount
        }
      }
    `)
    if (allTartansByLetter.errors) {
      throw allTartansByLetter.errors
    }
    const nodes = allTartansByLetter.data.allTartansCsv.nodes
    const totalCountByLetter = allTartansByLetter.data.allTartansCsv.totalCount
    const paginatedNodes = paginateNodes(nodes, pageLength)
    paginatedNodes.forEach((group, index, groups) => {
      createPage({
        path:
          index > 0 ? `/tartans/${letter}/${index + 1}` : `/tartans/${letter}`,
        component: path.resolve(`./src/templates/tartans.js`),
        context: {
          group,
          index,
          last: index === groups.length - 1,
          pageCount: groups.length,
          letter,
          previousLetterLastIndex,
        },
      })
    })
    previousLetterLastIndex = Math.ceil(totalCountByLetter / pageLength)
  }
}

The paginateNode function returns an array where initial elements are grouped by pageLength

const paginateNodes = (array, pageLength) => {
  const result = Array()
  for (let i = 0; i < Math.ceil(array.length / pageLength); i++) {
    result.push(array.slice(i * pageLength, (i + 1) * pageLength))
  }
  return result
}

Now let’s look into the tartan template. Since Gatsby is a React application, we can use the components that we were building in the first part of this article.

// ./src/templates/tartan.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import SvgTile from "../components/svgtile"
import SvgBg from "../components/svgbg"
import svgAsString from "../components/svgasstring"
import SvgDownloadLink from "../components/svgdownloadlink"
import PngDownloadLink from "../components/pngdownloadlink"

export const query = graphql`
  query($id: String!) {
    tartansCsv(id: { eq: $id }) {
      Palette
      Threadcount
      Origin_URL
      fields {
        slug
        Unique_Name
      }
    }
  }
`
const TartanTemplate = props => {
  const { fields, Palette, Threadcount } = props.data.tartansCsv
  const {slug} = fields
  const svg = SvgTile({
    palette: Palette,
    threadcount: Threadcount,
  })
  const svgData = svgAsString(svg)
  const svgSize = svg.props.width
  
  return (
    <Layout>
      <SvgBg svg={svg} />
      {/* title and navigation component comes here */}
      <div className="downloads">
        <SvgDownloadLink svgData={svgData} fileName={slug} />
        <PngDownloadLink svgData={svgData} size={svgSize} fileName={slug} />
      </div>
    </Layout>
  )
}
export default TartanTemplate

Finally let’s focus on the tartans index pages (the letter pages).

// ./src/templates/tartans.js
import React from "react"
import Layout from "../components/layout"
import {Link} from "gatsby"
import TartansNavigation from "../components/tartansnavigation"
const TartansTemplate = ({ pageContext }) => {
  const {
    group,
    index,
    last,
    pageCount,
    letter,
    previousLetterLastIndex,
  } = pageContext

  return (
    <Layout>
      <header>
        <h1>{letter}</h1>
      </header>
      <ul>
        {group.map(node => {
          return (
            <li key={node.fields.slug}>
              <Link to={`/tartan/${node.fields.slug}`}>
                <span>{node.fields.Unique_Name}</span>
              </Link>
            </li>
          )
        })}
      </ul>
      <TartansNavigation
        letter={letter}
        index={index}
        last={last}
        previousLetterLastIndex={previousLetterLastIndex}
      />
    </Layout>
  )
}
export default TartansTemplate

The TartansNavigation component adds next-previous navigation between the index pages.

// ./src/components/tartansnavigation.js
import React from "react"
import {Link} from "gatsby"

const letters = "abcdefghijklmnopqrstuvwxyz".split("")
const TartansNavigation = ({
  className,
  letter,
  index,
  last,
  previousLetterLastIndex,
}) => {
  const first = index === 0
  const letterIndex = letters.indexOf(letter)
  const previousLetter = letterIndex > 0 ? letters[letterIndex - 1] : ""
  const nextLetter =
    letterIndex < letters.length - 1 ? letters[letterIndex + 1] : ""
  
  let previousUrl = null, nextUrl = null

  // Check if previousUrl exists and create it
  if (index === 0 && previousLetter) {
    // First page of each new letter except "a"
    // If the previous letter had more than one page we need to attach the number 
    const linkFragment =
      previousLetterLastIndex === 1 ? "" : `/${previousLetterLastIndex}`
    previousUrl = `/tartans/${previousLetter}${linkFragment}`
  } else if (index === 1) {
    // The second page for a letter
    previousUrl = `/tartans/${letter}`
  } else if (index > 1) {
    // Third and beyond
    previousUrl = `/tartans/${letter}/${index}`
  }
  
  // Check if `nextUrl` exists and create it
  if (last && nextLetter) {
    // Last page of any letter except "z"
    nextUrl = `/tartans/${nextLetter}`
  } else if (!last) {
    nextUrl = `/tartans/${letter}/${(index + 2).toString()}`
  }

  return (
    <nav>
      {previousUrl && (
        <Link to={previousUrl} aria-label="Go to Previous Page" />
      )}
      {nextUrl && (
        <Link to={nextUrl} aria-label="Go to Next Page" />
      )}
    </nav>
  )
}
export default TartansNavigation

Final thoughts

Let’s stop here. I tried to cover all of the key aspects of this project. You can find all the tartanify.com code on GitHub. The structure of this article reflects my personal journey — understanding the specificity of tartans, translating them into SVG, automating the process, generating image versions, and discovering Gatsby to build a user-friendly website. It was maybe not as fun as our Scottish journey itself 😉, but I truly enjoyed it. Once again, a side project proved to be the best way to dig into new technology.

The post How We Created a Static Site That Generates Tartan Patterns in SVG appeared first on CSS-Tricks.

How We Tagged Google Fonts and Created goofonts.com

GooFonts is a side project signed by a developer-wife and a designer-husband, both of them big fans of typography. We’ve been tagging Google Fonts and built a website that makes searching through and finding the right font easier.

GooFonts uses WordPress in the back end and NuxtJS (a Vue.js framework) on the front end. I’d love to tell you the story behind goofonts.com and share a few technical details regarding the technologies we’ve chosen and how we adapted and used them for this project.

Why we built GooFonts

At the moment of writing this article, there are 977 typefaces offered by Google Fonts. You can check the exact number at any moment using the Google Fonts Developer API. You can retrieve the dynamic list of all fonts, including a list of the available styles and scripts for each family.

The Google Fonts website provides a beautiful interface where you can preview all fonts, sorting them by trending, popularity, date, or name. 

But what about the search functionality? 

You can include and exclude fonts by five categories: serif, sans-serif, display, handwriting, and monospace.

You can search within scripts (like Latin Extended, Cyrillic, or Devanagari (they are called subsets in Google Fonts). But you cannot search within multiple subsets at once.

You can search by four properties: thickness, slant, width, and "number of styles." A style, also called variant, refers both to the style (italic or regular) and weights (100, 200, up to 900). Often, the body font requires three variants: regular, bold, and italic. The “number of styles” property sorts out fonts with many variants, but it does not allow to select fonts that come in the “regular, bold, italic” combo.

There is also a custom search field where you can type your query. Unfortunately, the search is performed exclusively over the names of the fonts. Thus, the results often include font families uniquely from services other than Google Fonts. 

Let's take the "cartoon" query as an example. It results in "Cartoon Script" from an external foundry Linotype.

I can remember working on a project that demanded two highly stylized typefaces — one evoking the old Wild West, the other mimicking a screenplay. That was the moment when I decided to tag Google Fonts. :)

GooFonts in action

Let me show you how GooFonts works. The dark sidebar on the right is your  “search” area. You can type your keywords in the search field — this will perform an “AND” search. For example, you can look for fonts that are at once cartoon and slab. 

We handpicked a bunch of keywords — click any of them! If your project requires some specific subsets, check them in the subsets sections.  You can also check all the variants that you need for your font.

If you like a font, click its heart icon, and it will be stored in your browser’s localStorage. You can find your bookmarked fonts on the goofonts.com/bookmarks page. Together with the code, you might need to embed them.

How we built it: the WordPress part

To start, we needed some kind of interface where we could preview and tag each font. We also needed a database to store those tags. 

I had some experience with WordPress. Moreover, WordPress comes with its REST API,  which opens multiple possibilities for dealing with the data on the front end. That choice was made quickly.

I went for the most straightforward possible initial setup. Each font is a post, and we use post tags for keywords. A custom post type could have worked as well, but since we are using WordPress only for the data, the default content type works perfectly well.

Clearly, we needed to add all the fonts programmatically. We also needed to be able to programmatically update the fonts, including adding new ones or adding new available variants and subsets.

The approach described below can be useful with any other data available via an external API. In a custom WordPress plugin, we register a menu page from which we can check for updates from the API. For simplicity, the page will display a title, a button to activate the update and a progress bar for some visual feedback.

/**
 * Register a custom menu page. 
 */
function register_custom_menu_page() {
  add_menu_page( 
    'Google Fonts to WordPress', 
    'WP GooFonts', 
    'manage_options', 
    'wp-goofonts-menu', 
  function() { ?>        
    <h1>Google Fonts API</h1>
    <button type="button" id="wp-goofonts-button">Run</button>
    <p id="info"></p>        
    <progress id="progress" max="100" value="0"></progress>
  <?php }
  );
}
add_action( 'admin_menu', 'register_custom_menu_page' );


Let's start by writing the JavaScript part. While most of the examples of using Ajax with WordPress implements jQuery and the jQuery.ajax method, the same can be obtained without jQuery, using axios and a small helper Qs.js for data serialization.

We want to load our custom script in the footer, after loading axios and qs:

add_action( 'admin_enqueue_scripts' function() {
  wp__script( 'axios', 'https://unpkg.com/axios/dist/axios.min.js' );
  wp_enqueue_script( 'qs', 'https://unpkg.com/qs/dist/qs.js' );
  wp_enqueue_script( 'wp-goofonts-admin-script', plugin_dir_url( __FILE__ ) . 'js/wp-goofonts.js', array( 'axios', 'qs' ), '1.0.0', true );
});

Let’s look how the JavaScript could look like:

const BUTTON = document.getElementById('wp-goofonts-button')
const INFO = document.getElementById('info')
const PROGRESS = document.getElementById('progress')
const updater = {
  totalCount: 0,
  totalChecked: 0,
  updated: [],
  init: async function() {
    try {
      const allFonts = await axios.get('https://www.googleapis.com/webfonts/v1/webfonts?key=API_KEY&sort=date')
      this.totalCount = allFonts.data.items.length
      INFO.textContent = `Fetched ${this.totalCount} fonts.`
      this.updatePost(allFonts.data.items, 0)
    } catch (e) {
      console.error(e)
    }
  },
  updatePost: async function(els, index) {
    if (index === this.totalCount) {
      return
    }                
    const data = {
      action: 'goofonts_update_post',
      font: els[index],
    }
    try {
       const apiRequest = await axios.post(ajaxurl, Qs.stringify(data))
       this.totalChecked++
       PROGRESS.setAttribute('value', Math.round(100*this.totalChecked/this.totalCount))
       this.updatePost(els, index+1)
    } catch (e) {
       console.error(e)
      }
   }
}

BUTTON.addEventListener('click', () => {
  updater.init()
})

The init method makes a request to the Google Fonts API. Once the data from the API is available, we call the recursive asynchronous updatePost method that sends an individual font in the POST request to the WordPress server.

Now, it’s important to remember that WordPress implements Ajax in its specific way. First of all, each request must be sent to wp-admin/admin-ajax.php. This URL is available in the administration area as a global JavaScript variable ajaxurl.

Second, all WordPress Ajax requests must include an action argument in the data. The value of the action determines which hook tag will be used on the server-side.

In our case, the action value is goofonts_update_post. That means what happens on the server-side is determined by the wp_ajax_goofonts_update_post hook.

add_action( 'wp_ajax_goofonts_update_post', function() {
  if ( isset( $_POST['font'] ) ) {
    /* the post tile is the name of the font */
    $title = wp_strip_all_tags( $_POST['font']['family'] );
    $variants = $_POST['font']['variants'];
    $subsets = $_POST['font']['subsets'];
    $category = $_POST['font']['category'];
    /* check if the post already exists */
    $object = get_page_by_title( $title, 'OBJECT', 'post' );
    if ( NULL === $object ) {
      /* create a new post and set category, variants and subsets as tags */
      goofonts_new_post( $title, $category, $variants, $subsets );
    } else {
      /* check if $variants or $subsets changed */
      goofonts_update_post( $object, $variants, $subsets );
    }
  }
});

function goofonts_new_post( $title, $category, $variants, $subsets ) {
  $post_id =  wp_insert_post( array(
    'post_author'  =>  1,
    'post_name'    =>  sanitize_title( $title ),
    'post_title'   =>  $title,
    'post_type'    =>  'post',
    'post_status'  => 'draft',
    )
  );
  if ( $post_id > 0 ) {
    /* the easy part of tagging ;) append the font category, variants and subsets (these three come from the Google Fonts API) as tags */
    wp_set_object_terms( $post_id, $category, 'post_tag', true );
    wp_set_object_terms( $post_id, $variants, 'post_tag', true );
    wp_set_object_terms( $post_id, $subsets, 'post_tag', true );
  }
}

This way, in less than a minute, we end up with almost one thousand post drafts in the dashboard — all of them with a few tags already in place. And that’s the moment when the crucial, most time-consuming part of the project begins. We need to start manually add tags for each font one by one.
The default WordPress editor does not make much sense in this case. What we needed is a preview of the font. A link to the font’s page on fonts.google.com also comes in handy.

custom meta box does the job very well. In most cases, you will use meta boxes for custom form elements to save some custom data related to the post. In fact, the content of a meta box can be practically any HTML.

function display_font_preview( $post ) {
  /* font name, for example Abril Fatface */
  $font = $post->post_title;
  /* font as in url, for example Abril+Fatface */
  $font_url_part = implode( '+', explode( ' ', $font ));
  ?>
  <div class="font-preview"> 
    <link href="<?php echo 'https://fonts.googleapis.com/css?family=' . $font_url_part . '&display=swap'; ?>" rel="stylesheet">
    <header>
      <h2><?php echo $font; ?></h2>
      <a href="<?php echo 'https://fonts.google.com/specimen/' . $font_url_part; ?>" target="_blank" rel="noopener">Specimen on Google Fonts</a>
    </header>
    <div contenteditable="true" style="font-family: <?php echo $font; ?>">
      <p>The quick brown fox jumps over a lazy dog.</p>
      <p style="text-transform: uppercase;">The quick brown fox jumps over a lazy dog.</p>
      <p>1 2 3 4 5 6 7 8 9 0</p>
      <p>& ! ; ? {}[]</p>
    </div>
  </div>
<?php }

add_action( 'add_meta_boxes', function() {
  add_meta_box(
    'font_preview', /* metabox id */
    'Font Preview', /* metabox title */
    'display_font_preview', /* content callback */
    'post' /* where to display */
  );
});

Tagging fonts is a long-term task with a lot of repetition. It also requires a big dose of consistency. That’s why we started by defining a set of tag “presets.” That could be, for example:

{
  /* ... */
  comic: {
    tags: 'comic, casual, informal, cartoon'
  },
  cursive: {
    tags: 'cursive, calligraphy, script, manuscript, signature'
  },
  /* ... */
}

Next with some custom CSS and JavaScript, we “hacked” the WordPress editor and tags form by enriching it with the set of preset buttons. 

How we built it: The front end part (using NuxtJS)

The goofonts.com interface was designed by Sylvain Guizard, a french graphic and web designer (who also happens to be my husband). We wanted something simple with a distinguished “search” area. Sylvain deliberately went for colors that are not too far from the Google Fonts identity. We were looking for a balance between building something unique and original while avoiding user confusion.

While I did not hesitate choosing WordPress for the back-end, I didn’t want to use it on front end. We were aiming for an app-like experience and I, personally, wanted to code in JavaScript, using Vue.js in particular.

I came across an example of a website using NuxtJS with WordPress and decided to give it a try. The choice was made immediately. NuxtJS is a very popular Vue.js framework, and I really enjoy its simplicity and flexibility. 
I’ve been playing around with different NuxtJS settings to end up with a 100% static website. The fully static solution felt the most performant; the overall experience seemed the most fluid.That also means that my WordPress site is only used during the build process. Thus, it can run on my localhost. This is not negligible since it eliminates the hosting costs and most of all, lets me skip the security-related WordPress configuration and relieves me of the security-related stress. ;)

If you are familiar with NuxtJS, you probably know that the full static generation is not (yet) a part of NuxtJS. The prerendered pages try to fetch the data again when you are navigating.

That’s why we have to somehow “hack” the 100% static generation. In this case, we are saving the useful parts of the fetched data to a JSON file before each build process. This is possible, thanks to Nuxt hooks, in particular, its builder hooks.

Hooks are typically used in Nuxt modules:

/* modules/beforebuild.js */

const fs = require('fs')
const axios = require('axios')

const sourcePath = 'http://wpgoofonts.local/wp-json/wp/v2/'
const path = 'static/allfonts.json'

module.exports = () => {
  /* write data to the file, replacing the file if it already exists */
  const storeData = (data, path) => {
    try {
      fs.writeFileSync(path, JSON.stringify(data))
    } catch (err) {
      console.error(err)
    }
  }
  async function getData() {    
    const fetchedTags = await axios.get(`${sourcePath}tags?per_page=500`)
      .catch(e => { console.log(e); return false })
    
  /* build an object of tag_id: tag_slug */
    const tags = fetchedTags.data.reduce((acc, cur) => {
      acc[cur.id] = cur.slug
      return acc
    }, {})
    
  /* we want to know the total number or pages */
    const mhead = await axios.head(`${sourcePath}posts?per_page=100`)
      .catch(e => { console.log(e); return false })
    const totalPages = mhead.headers['x-wp-totalpages']

  /* let's fetch all fonts */
    let fonts = []
    let i = 0
    while (i < totalPages) {
      i++
      const response = await axios.get(`${sourcePath}posts?per_page=100&page=${i}`)
      fonts.push.apply(fonts, response.data)
    }
  
  /* and reduce them to an object with entries like: {roboto: {name: Roboto, tags: ["clean","contemporary", ...]}} */
    fonts = (fonts).reduce((acc, el) => {
      acc[el.slug] = {
        name: el.title.rendered,
        tags: el.tags.map(i => tags[i]),
      }
      return acc
    }, {})

  /* save the fonts object to a .json file */
    storeData(fonts, path)
  }

  /* make sure this happens before each build */
  this.nuxt.hook('build:before', getData)
}
/* nuxt.config.js */
module.exports = {
  // ...
  buildModules: [
    ['~modules/beforebuild']
  ],
// ...
}

As you can see, we only request a list of tags and a list posts. That means we only use default WordPress REST API endpoints, and no configuration is required.

Final thoughts

Working on GooFonts was a long-term adventure. It is also this kind of projects that needs to be actively maintained. We regularly keep checking Google Fonts for the new typefaces, subsets, or variants. We tag new items and update our database. Recently, I was genuinely excited to discover that Bebas Neue has joint the family. We also have our personal favs among the much lesser-known specimens.

As a trainer that gives regular workshops, I can observe real users playing with GooFonts. At this stage of the project, we want to get as much feedback as possible. We would love GooFonts to be a useful, handy and intuitive tool for web designers. One of the to-do features is searching a font by its name. We would also love to add a possibility to share the bookmarked sets and create multiple "collections" of fonts.

As a developer, I truly enjoyed the multi-disciplinary aspect of this project. It was the first time I worked with the WordPress REST API, it was my first big project in Vue.js, and I learned so much about typography.  

Would we do anything differently if we could? Absolutely. It was a learning process. On the other hand, I don't think we would change the main tools. The flexibility of both WordPress and Nuxt.js proved to be the right choice. Starting over today, I would definitely took time to explore GraphQL, and I will probably implement it in the future.

I hope that you find some of the discussed methods useful. As I said before, your feedback is very precious. If you have any questions or remarks, please let me know in the comments!

The post How We Tagged Google Fonts and Created goofonts.com appeared first on CSS-Tricks.