GooFonts is a side project built by a developer-wife and designer-husband team, both typography enthusiasts. Frustrated with the limitations of Google Fonts' own search, they began manually tagging the entire catalog of typefaces and built a dedicated search tool powered by WordPress and NuxtJS.

The project stems from a concrete problem: the Google Fonts website lets you filter by category or script, but you can't combine those filters or search across multiple subsets simultaneously. Standard search only matches font names. For example, querying "cartoon" returns "Cartoon Script" from the external foundry Linotype — a result that has nothing to do with Google Fonts.

At the time of writing, the Google Fonts Developer API lists 977 typefaces. That's a lot of fonts to evaluate when a project needs something specific — like the time the author needed one typeface evoking the old Wild West and another mimicking a screenplay, and there was no practical way to find them.

How GooFonts Works

The search interface uses a dark sidebar. Keywords typed there perform an "AND" search, so you can find fonts that are both cartoon and slab, for instance. You can also filter by individually curated keyword tags, check required script subsets, and select the specific variants you need.

Registered users can heart fonts they like, and these bookmarks are stored in the browser's localStorage. The goofonts.com/bookmarks page lets you review saved fonts later.

WordPress as a Tagging Backend

The author chose WordPress as a backend because it was familiar and ships with a REST API. Each font is a built-in post type, with post tags serving as the keywords. A custom post type would also work, but the default content type suffices when WordPress is used only as a data store.

Adding fonts programmatically was a key requirement. The author built a custom WordPress plugin with a menu page containing a title, a button, and a progress bar for visual feedback. The update process runs in JavaScript using axios and the data serialization helper Qs.js (rather than the more common jQuery/jQuery.ajax approach). The script is loaded in the footer:

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 );
});

The script's init method makes a request to the Google Fonts API. Once data is available, a recursive asynchronous updatePost method sends each font as a POST request to the WordPress server.

All WordPress Ajax requests must go to wp-admin/admin-ajax.php, available as the global JavaScript variable ajaxurl. Every request must include an action argument; the action value goofonts_update_post triggers the server-side 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 );
  }
}

In less than a minute, the script creates nearly one thousand post drafts, each with a few tags. That's when the slow, meticulous part begins: manually tagging every font. For this task, the default WordPress editor is nearly useless — what's needed is a font preview plus a quick link to the font's Google Fonts page. A custom meta box works perfectly since its content can be 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 */
  );
});

Maintaining consistency across long tagging sessions demanded a system of "presets." The author defined a set of reusable tag combinations, then extended the WordPress tag-editing screen with custom CSS and JavaScript to render preset buttons. This "hacking" of the standard editor made the workflow both faster and more consistent.

The NuxtJS Front End

The front end is designed by Sylvain Guizard to have what the author calls a simple aesthetic, with colors that stay close to Google Fonts' identity to avoid confusing users.

The WordPress REST API powers the back end, but the front end is intentionally not WordPress-generated. It runs on NuxtJS, chosen because the author wanted a purely Vue.js codebase and was drawn to Nuxt's simplicity

. A static site felt the most performant and gave the app-like feel they were after.

This architecture means WordPress only runs during the build process — often on the author's localhost, eliminating hosting costs and sidestepping most security configuration entirely.

The catch: full static generation isn't a native NuxtJS feature. Prerendered pages would normally attempt to re-fetch data during client-side navigation. To force a 100% static site, the author saves useful parts of the fetched data into a JSON file before each build. This is accomplished via Nuxt's hooks, specifically builder hooks:

/* 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']
  ],
// ...
}

The build fetches only a list of tags and a list of posts, using WordPress default REST API endpoints with no WordPress-side configuration. Everything runs from the nuxt.generate() method that performs the static generation.

Maintaining the Project

A site like this demands active upkeep — new Google Fonts, subsets, and variants appear regularly and need tagging. Goofonts' personal favorites include relatively obscure specimens, but the author admitted to genuine excitement when the well-known Bebas Neue joined the catalog.

Features in the pipeline include searching by font name and sharing bookmarked sets or creating multiple font "collections."

The author notes this was their first experience with the WordPress REST API, and their first major Nuxt.js project. If they were starting over, they would explore GraphQL before committing to their REST-based architecture — a technology they expect to implement eventually.

Despite the aches and lessons, the core tools still feel right. For a project that needs a flexible backend plus an app-speed front end, WordPress and NuxtJS delivered.