Exploring Nuxt 3 and Storyblok

created: Nov 27, 2022 / last updated: Sep 11, 2023

Pretty neat when it works although there were some frustrating moments.

Exploring new technologies can be both exciting and challenging, and my recent foray into Nuxt 3 and Storyblok was no exception. Inspired by a slick and seemingly easy to reproduce demo at a Vue conference in Toronto, I decided it was time to give my aging personal website a much-needed makeover and dive into this intriguing pairing.

After some extensive Googling and a deep dive into the Nuxt 3 and Storyblok module documentation, I stumbled upon a tutorial that proved to be a very helpful. However, there were a few important details that it didn't cover. One of these crucial details was the "apiOptions" property in the configuration. Without setting this field to 'us' in your nuxt.config.js file, if you're residing in America, things won't work as expected. A special shoutout to GitHub user 'Tidwell' for helping the community discover this fix. Another lesson learned was to ignore the advice at the beginning of the tutorial about disabling TypeScript. You don't have to use TypeScript if it's enabled, but you'll reap the benefits, such as being able to use CTRL+Space to access a wealth of contextually relevant information at your cursor's position.

//nuxt.config.js
export default defineNuxtConfig({
  modules: [
    [
      "@storyblok/nuxt",
      {
        accessToken: process.env.STORYBLOK_TOKEN,
        apiOptions: { region: "us" }, //This field was not mentioned in the tutorial and is important
      },
    ],
  ],
});

The rest of the tutorial went relatively smoothly, though there was one more tip to keep in mind. What's referred to as a 'hint' is practically mandatory for the system to work seamlessly. You need to configure your localhost to use HTTPS, or the preview system will break.

Building the New Site

My vision for the new site was to create a simple but nice looking blog, a printable or CSV-convertible resume page, and perhaps a few additional pages showcasing my past work. I decided to kick things off with the blog because I wanted to document the process of building it.

The core concept behind a Storyblok and Nuxt setup is to create blocks and then build components to render those blocks. At the root of your repository, you should have a /storyblok folder where everything mirrors a block. So far, I've utilized two types of blocks: 'Nestable' and 'Content Type'. As I understand it, 'Content Type' is designed for use in the "Content" section of the Storyblok dashboard.

Content Type image

I've categorized my "Content" blocks into two types: "Page" for route-level pages like "Main" and "Music," and "Blog" for each new blog entry. Additionally, there are "Nestable" blocks.

Currently, I'm using three of these blocks to construct my pages, but I'm primarily focusing on the "All Articles" block.

Syntax Highlighting

As someone who hasn't delved much into Vue 3 yet, this project presented an opportunity to explore it. My initial goal was to implement syntax highlighting for code within my blog posts, and this proved to be quite a challenge. I spent considerable time attempting to replace the default rendering of RichTextField with custom components, but after hours of struggle, I decided to explore other options. I discovered that you can create custom components that can be added within your rich text field, which is a neat feature I'll likely explore later. For now, I opted to keep things simple and stick with the Rich Text Editor while experimenting with composables and highlights.js.

The first step was to install the highlights.js npm package, and then creating the composable was a breeze. In the root of my project, I established a /composables folder and crafted a file named syntax.js. Here's the code for my very first composable:

// ~/composables/syntax.js

import { onMounted } from "vue";
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import xml from "highlight.js/lib/languages/xml";
import json from "highlight.js/lib/languages/json";
import "highlight.js/styles/gradient-dark.css";
start with "use"

export function useSyntax() {
  hljs.registerLanguage("vue", xml);
  hljs.registerLanguage("javascript", javascript);
  hljs.registerLanguage("json", json);
  hljs.configure({
    languages: ["javascript", "vue", "json"],
  });

  onMounted(() => {
    hljs.highlightAll();
  });
}

Google Fonts

Integrating Google Fonts turned out to be a bit pesky. While there is a Nuxt Google Fonts module available, its documentation left something to be desired and was inconsistent across its npm package page, documentation page, and GitHub readme. Eventually, I managed to make it work, but only with certain fonts. In the end, I opted for the Google Font CSS @import option to keep things running smoothly.

/** ~/assets/css/main.css **/
@import url("https://fonts.googleapis.com/css2?family=Lilita+One&family=Merriweather:wght@300;400;900&family=PT+Mono&display=swap");

By the way, here's how to import a universal custom CSS sheet into Nuxt: simply add it in a style property in the nuxt.config.js file, like so:

export default defineNuxtConfig({
  modules: [
    [
      "@storyblok/nuxt",
      {
        accessToken: process.env.STORYBLOK_TOKEN,
        apiOptions: { region: "us" },
        useApiClient: true,
      },
    ],
  ],
  target: "static",
  css: ["@/assets/css/main.css"],
});

Exploring Pinia

I've been eager to learn Pinia, and this project presented an opportunity to get acquainted with it. My site's navigation consists of two separate components that need to communicate with each other. While it might seem like overkill because I could easily do this with a composable, it was an excellent exercise to set up a Pinia store for this purpose. However, the challenge arose again when dealing with documentation. There's a well-structured page on the Pinia site for integrating it with Nuxt, but for some reason, npm refused to install the Pinia library required for Nuxt integration. The only workaround that worked for me was to force the installation, which was less than ideal but ultimately effective. Here's a snippet

export const useNavStore = defineStore({
  id: "nav-store",
  state: () => {
    return {
      navOpen: false,
    };
  },
  actions: {
    openMenu() {
      this.navOpen = true;
    },
    closeMenu() {
      this.navOpen = false;
    },
    toggleMenu() {
      this.navOpen = !this.navOpen;
    },
  },
  getters: {
    nav: (state) => state.navOpen,
  },
});

I imported and initialized it as follows:

<template>
  <main>
    <Hamburger class="fixed top-10 right-10" @show-menu="store.toggleMenu" />
    <Nav />
    <slot />
  </main>
</template>

<script setup>
import { useNavStore } from "~/store/nav";
let store = useNavStore();
</script>

Current nuxt.config

Configuring Nuxt can be tricky, so here's my current setup for those embarking on similar journeys:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  transition: "page",
  generate: {
    fallback: true,
  },
  target: "static",
  css: ["@/assets/css/main.css"],
  modules: [
    [
      "@storyblok/nuxt",
      {
        accessToken: process.env.STORYBLOK_TOKEN,
        apiOptions: { region: "us" },
        useApiClient: true,
      },
    ],
    [
      "@pinia/nuxt",
      {
        autoImports: ["defineStore"],
      },
    ],
    "@nuxt/image-edge",
    "@nuxtjs/tailwindcss",
  ],
});

To Be Continued

If you're curious about my progress so far, you can check it out here. My next steps include improving my Lighthouse scores, deploying the site, and possibly adding a comments section to blog posts.

Feel free to customize and expand upon this revised version of your blog post as needed. If you have any specific questions or want to focus on particular aspects of your project, please let me know, and I can provide more detailed information or guidance.