After Nuxt, What's Next? πŸ’­

Cover Image

Published on dev.to and medium.com

New post, new topic, this time I take you into the thick of JAMStack to talk to you about SSG, SSR, SEO, through Nuxt and Next frameworks, not to mention the wonderful world of UnifiedJS.

You will understand, here I'll talk about... JavaScript (once more time πŸ˜‰). But before that, a small introduction to contextualize things...

Previously...

In early 2022, I started implementing a translation feature for my portfolio.

NB: At the origin, my portfolio was only translated into English, and those around me kept asking me to have it available in my native language, i.e. French.

My project being initially developed from a JAMStack framework, I'm oriented to an already existing "i18n" plugin. After setting it, I quickly realized that it didn't fit my needs perfectly. Indeed, I wanted a "hybrid" mode allowing me to translate simply (via a classic "key - value" system), but also to be able to translate by myself (especially for posts). So I had to (re)code part of the utility in order to achieve a suitable result... But still, far from being optimized.

Following this observation, I started a migration work, because even if I had more flexibilities, I might as well test several other technologies in detail! So I went from Gridsome to Next (via Gatsby, then Nuxt).

NB: My heart is torn between React and Vue since years, this time I let myself be tempted by React (it's also the library that I mostly use every day, so one more point in favor of it).

This work lasted until February (between comparisons, migration, implementation of internationalization, tests, etc...) Anyway! What to enjoy and (re)discover modern and efficient technologies.

I transcribe here (in the form of a series), some advantages and disadvantages that I have been able to identify for the use of each of these frameworks.

WTF Is JAMStack!?

As a reminder, JAMStack is a technical environment that consists of building a website / application from JavaScript, reusable APIs and serving it in HTML format ("M" is for Markup) using a static site generator.

The data that will be used to feed the website / application can be retrieved locally (via Markdown files for example), or remotely, via CMS APIs. The static site generator then builds a stable release (including all necessary resources and pages) ready to be uploaded on a hosting service.

This technical environment offers many advantages, such as better responsiveness (due to the recovery of all resources during the build phase), better scalability (the developer is not constrained by a heavy architecture, he can focus on the frontend), and especially a better SEO (each page can manage its attributes related to SEO).

NB: JAMStack frameworks are built around the PRPL pattern, to optimize the performance of websites / applications on smartphones and devices with a weak internet connection.

Ep 1. Vue + JAMStack = Gridsome

At the middle of this ecosystem is the Gridsome open-source framework, powered by the Vue community. As for this last one, it benefits from an active developer network and well-done documentation.

npm install -g @gridsome/cli
gridsome create my-portfolio

The Gridsome CLI makes scaffolding your JAMStack project architecture very simply. Furthermore, the real power of this kind of framework lies in its tree structure of files / folders which brings the routing to the highest level.

<template>
  <Layout>
    <div class="post-title">
      <h1>{{ $page.post.title }}</h1>

      <PostMeta :post-date="$page.post.date" :time-to-read="$page.post.timeToRead" />
    </div>

    <div class="post">
      <div class="post__header">
        <g-image v-if="$page.post.coverImage" alt="Cover Image" :src="$page.post.coverImage" />
      </div>

      <div class="post__content" v-html="$page.post.content" />

      <PostTags :post-tags="$page.post.tags" />
    </div>
  </Layout>
</template>

<page-query>
  query Post($id: ID!) {
    post: post(id: $id) {
      content
      title
      date(format: "YYYY-MM-DD")
      description
      coverImage(width: 720, blur: 10)
      tags
      path
      timeToRead
    }
  }
</page-query>

<script>
  import PostMeta from '~/components/PostMeta.vue';
  import PostTags from '~/components/PostTags.vue';

  export default {
    components: {
      PostMeta,
      PostTags
    },
    metaInfo() {
      return {
        title: this.$page.post.title,
        meta: [
          {
            name: 'description',
            content: this.$page.post.description
          }
        ]
      };
    }
  };
</script>

Gridsome has a (magic) API in GraphQL format (here between <page-query> tags) to retrieve content and integrate it into the component, through the $page variable. In addition, it embeds a part of RemarkJS (🚨 #SpoilerAlert 🚨 Cf. The Wonderful World Of UnifiedJS) inside its API, to transform Markdown files into HTML format.

This framework also includes the vue-meta dependency for metadata management. So, it's very easy to add or update the data responsible for the good SEO of your website, and this for each "page" or "template" type component.

As mentioned earlier, project structure matters, since components placed in the "pages" folder, will create their own routes according to their naming (in practice, a 404.vue file will create a /404 page). On the other hand, for the generation of pages on the fly, it's better to use the "templates" folder.

+-- content                     # *.md Are Here
+-- public                      # Static Files
+-- src
    +-- components
    +-- layouts
    +-- pages                   # Explicit Pages
    +-- templates               # Dynamic Page Templates
+-- gridsome.config.js
+-- gridsome.server.js
+-- package.json

Still in the framework architecture, the gridsome.server.js file is used to manipulate the Gridsome API, especially to create dynamic pages (based on "templates" components). Among the use cases, there are dependencies related to Gridsome; example with "sources" plugins that load data (asynchronously) and make them available from the GraphQL interface.

Finally, the gridsome.config.js file speaks for itself, since it allows to enrich the project's configuration, whether it's the title, the description of the website (in a SEO context), etc... Or to integrate additional libraries (support for "i18n" locales for example).

NB: That may be the downside of this kind of technology. Many turnkey features come from plugins (with their own configuration). Unfortunately, if you want to go deeper with a library, there's no other way than to pull the project locally and (re)develop part of the code; otherwise settle for the basic features...

Gridsome is a small nugget in the world of web development. It benefits from a strong community, as well as many "starters" which serve as a basis for the development of a new website. If you start with a JAMStack framework, it will quickly be ready to use, to expose local data (in .md, .mdx formats) or remote, from a CMS interface (Strapi, Forestry or Contentful).

It met my need for 2 years, but now it's time to change...

Ep 2. The Great, Gatsby ✨

Gatsby is the dark side of the force (if you consider Gridsome as its bright side). In other words, Gatsby is the equivalent of this last one in the React ecosystem.

npm install -g gatsby-cli
gatsby new

Just like its counterpart, Gatsby has a CLI tool to build a new JAMStack project. The difference is that it works with a "questions - answers" system. So, you can choose to add the support for Markdown files, to integrate a UI library (styled-component / emotion), but also to configure the use of a CMS.

It has a lot of concepts in common with Gridsome, especially for the routing management through the "pages" folder, dynamizing pages using the "templates" folder convention, retrieving local or remote data via a GraphQL API, etc...

import React from 'react';
import { Helmet } from 'react-helmet';
import { graphql } from 'gatsby';
import { GatsbyImage } from 'gatsby-plugin-image';
import Layout from '@/components/Layout';
import PostMeta from '@/components/PostMeta';
import PostTags from '@/components/PostTags';

export default function Post({ data: { post } }) {
  const { frontmatter, fields } = post;
  const { childImageSharp } = frontmatter.coverImage;

  return (
    <>
      <Helmet>
        <title>{frontmatter.title}</title>
        <meta name="description" content={frontmatter.description} />
      </Helmet>

      <Layout>
        <div className="post-title">
          <h1>{frontmatter.title}</h1>

          <PostMeta postDate={frontmatter.date} readingTime={fields.readingTime} />
        </div>

        <div className="post">
          <div className="post__header">
            {frontmatter.coverImage && (
              <GatsbyImage
                alt="Cover Image"
                src={childImageSharp.gatsbyImageData}
              />
            )}
          </div>

          <div className="post__content" dangerouslySetInnerHTML={{ __html: post.html }} />

          <PostTags postTags={frontmatter.tags} />
        </div>
      </Layout>
    </>
  );
}

export const query = graphql`
  query Post($id: ID!) {
    post: markdownRemark(id: { eq: $id }) {
      html
      frontmatter {
        title
        date(formatString: "YYYY-MM-DD")
        description
        coverImage {
          childImageSharp {
            gatsbyImageData(quality: 90, width: 720, formats: [WEBP])
          }
        }
        tags
      }
      fields {
        slug
        readingTime {
          minutes
        }
      }
    }
  }
`;

Here, you notice the use of a GraphQL API (again) to inject data as component props (even if the syntax differs somewhat from Gridsome, it's basically the same structure). Thanks to the gatsby-transformer-remark dependency (🚨 #SpoilerAlert 🚨 Cf. The Wonderful World Of UnifiedJS), previously installed when querying the CLI, the framework becomes able to exploit files in .md format.

NB: I also had to add the gatsby-remark-reading-time plugin to get the reading time per post, where Gridsome did it implicitly.

This framework supports modern image formats (WebP) very well, ideal for optimizing the refresh time of a website. For SEO, it will be necessary to go through an additional library (especially react-helmet), to apply the metadata on the different pages.

Gatsby's strong point is its SaaS mode. If you don't want to deploy your application on a traditional web server (Apache / Nginx), there are alternatives JAMStack solutions, such as Netlify or Vercel, but also... Gatsby Cloud! The framework has its own product for an optimal experience! πŸ‘Œ

I use Gatsby since version 2.0 with the Orluk Photography project. I have never been disappointed by this tool, it supports TypeScript quite well (better since version 3.0), and interfaces perfectly with a CMS (Strapi, I love you πŸ’œ). But, given the similarities with Gridsome, you might as well keep this last one; or try something new...

Ep 3. Nuxt : One "Meta" Framework To Rule Them All!

Just as popular as Gatsby *, there's Nuxt! I always wanted to try this framework, and I must say that Debbie O'Brien's posts confirmed my enthusiasm for this library of the Vue ecosystem.

Nuxt perfectly embraces the JAMStack philosophy, but it does much more than that. Indeed, it has three modes of operation:

  • The Single Page App mode (SPA for friends);
  • The "static" mode (SSG), allowing to build the application using a static site generator;
  • The "universal" mode, which allows the application to be rendered via a NodeJS server.

With Server Side Rendering, the user will access the website faster than in CSR mode. The Client Side Rendering relies on JavaScript to provide the HTML; while SSR mode first provides the static content (i.e. HTML), then the JavaScript, etc... Aside from performance gains, this mode of operation allows indexing robots to browse the website more easily (since the pages are directly accessible).

Anyway! It was time to play with this framework!!! πŸ”₯

NB: Version 3.0 has just been released in public beta, I preferred to develop from a stable version that has already proven itself.

npx create-nuxt-app my-portfolio

As for Gatsby, the Nuxt CLI is simply great because it allows you to initialize a project with a full configuration. You can choose: the JavaScript or TypeScript language, the SSG or SSR mode, the CSS framework to use (including TailwindCSS), the unit tests engine, the Prettier implementation, etc...

Nuxt has many assets, including the Vuex integration by default (allowing to manage data using the "state management" pattern for applications at scale), but most importantly a file-based browsing system (which is not unlike that of Gridsome), with the famous "pages" folder.

However, for fetching data, this is a different story. There's no more GraphQL API to rely on. This time, things have to be done from scratch! Maybe not...

<template>
  <Layout>
    <div class="post-title">
      <h1>{{ post.title }}</h1>

      <PostMeta :post-date="post.date" :reading-time="post.readingTime" />
    </div>

    <div class="post">
      <div class="post__header">
        <img v-if="post.coverImage" :src="post.coverImage" alt="Cover Image" width="720" height="405" />
      </div>

      <nuxt-content class="post__content" :document="post" />

      <PostTags :post-tags="post.tags" />
    </div>
  </Layout>
</template>

<script>
  import PostMeta from '~/components/PostMeta.vue';
  import PostTags from '~/components/PostTags.vue';

  export default {
    components: {
      Layout,
      PostMeta,
      PostTags
    },
    async asyncData({ app, $content, params }) {
      const post = await $content(params.slug).fetch();
      return { post };
    },
    head() {
      return {
        title: this.post.title,
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: this.post.description
          }
        ]
      };
    }
  };
</script>

To help me access and read my Markdown files (and turn them into Markup), I used one of the many Nuxt community modules, namely @nuxt/content. Now, thanks to an API accessible by the $content variable, I'm able to retrieve the front-matter and the content of my .md files to use them inside my <template>.

Apart from this 1st integration, I also had to add a dependency for SEO feed (npm i vue-meta), a second dependency for the translation functionality (npm i vue-i18n), as well as utility functions (such as reading time calculation).

NB: To calculate the reading time, I created a simple function that I then injected into the $content API, thanks to a "hook" in the Nuxt configuration.

import { readingTime } from './src/utils';

export default {
  // ...nuxt.config.js
  hooks: {
    'content:file:beforeInsert': document => {
      if (document.extension === '.md') {
        document.readingTime = readingTime(document.text);
      }
    }
  }
};

After having correctly configured my Nuxt environment, and (re)developing my dynamic pages, I carried out performance tests with Google Lighthouse, and I realized that some points could be optimized, especially for the image management (score ~= 70). Again, I had to install another open-source module (@nuxt/images / nuxt-optimized-images), to support WebP format.

Verdict? Nuxt is really cool! I fell in love with its SSR mode. Unfortunately it requires a few tweaks (here and there) to be fully operational / effective. Okay, what's next...

Ep 4. What's Next? πŸ’­ #SeasonFinale

I (re)discovered Next during their conference last October. There's so much to say about this framework...

NB: Starting with the fact that Svelte's creator, Rich Harris, joined the Vercel teams recently πŸ‘

Popularized by React, this framework is the equivalent of Nuxt. It benefits from similar concepts, such as page management by the same name folder. The difference is that the dependencies added to Next will be more like "standard" JavaScript libraries rather than framework-related plugins (after all, React is a JavaScript library, not a framework 😎).

npx create-next-app

Lighter than its counterparts, the CLI tool simply generates the project tree (including react, react-dom and next). Next focuses on a SSR deployment rather than CSR (although possible with the next export command). So, it will compile the necessary resources and then serve them on the server side.

+-- content                     # *.md Are Here
+-- public                      # Static Files
+-- src
    +-- components
    +-- pages                   # Explicit Pages
    +-- services                # Data Fetching
    +-- utils
+-- next.config.js
+-- package.json

Above is the structure I use for my portfolio project. There's very little configuration in the next.config.js file, I only registered my locales there for my internationalization feature, as well as the configuration of the PWA mode (but this is another story).

import Head from 'next/head';
import Image from 'next/image';
import Layout from '@/components/Layout';
import PostMeta from '@/components/PostMeta';
import PostTags from '@/components/PostTags';
import { getPostBySlug, getAllPostSlugs } from '@/services/contentService';
import { markdownToHtml } from '@/utils/markdownUtil';

export default function Post({ post }) {
  return (
    <>
      <Head>
        <title>{post.title}</title>
        <meta name="description" content={post.description} />
      </Head>

      <Layout>
        <div className="post-title">
          <h1>{post.title}</h1>

          <PostMeta postDate={post.date} timeToRead={post.timeToRead} />
        </div>

        <div className="post">
          <div className="post__header">
            {post.coverImage && (
              <Image alt="Cover Image" src={post.coverImage} width={720} height={405} />
            )}
          </div>

          <div className="post__content" dangerouslySetInnerHTML={{ __html: post.content }} />

          <PostTags postTags={post.tags} />
        </div>
      </Layout>
    </>
  );
}

export const getStaticProps = async ({ params: { slug } }) => {
  const post = getPostBySlug(slug, [
    'content',
    'title',
    'date',
    'description',
    'coverImage',
    'tags',
    'timeToRead'
  ]);
  const content = await markdownToHtml(post.content);

  return {
    props: {
      post: {
        slug,
        ...post,
        content
      }
    }
  };
};

export const getStaticPaths = async () => {
  const allPostSlugs = getAllPostSlugs();

  return {
    paths: allPostSlugs.map((slug) => ({
      params: {
        slug
      }
    })),
    fallback: false
  };
};

Next doesn't have a ready-to-use GraphQL API, nor modules for exploiting .md / .mdx formats; it's up to the developer to code the functions he needs. Thanks to the use of NodeJS, and the winning combo of its fs and path modules, it's possible to access the file system. Then, you will have to do some transformations with RemarkJS (🚨 #SpoilerAlert 🚨 Cf. The Wonderful World Of UnifiedJS) to expose the content of Markdown files in HTML format.

import fs from 'fs';
import join from 'path';
import matter from 'gray-matter';
import { getReadingTime } from '@/utils';

export const getPostBySlug = (slug, fields = []) => {
  const realSlug = slug.replace(/\.md$/, '');
  const postsDir = path.join(process.cwd(), 'content');
  const fullPath = path.join(postsDir, `${realSlug}.md`);
  const file = fs.readFileSync(fullPath, 'utf-8');
  const { data, content } = matter(file);

  const item = {};

  fields.forEach((field) => {
    if (field === 'slug') {
      item[field] = realSlug;
    }

    if (field === 'content') {
      item[field] = content;
    }

    if (field === 'timeToRead') {
      item[field] = getReadingTime(content);
    }

    if (typeof data[field] !== 'undefined') {
      item[field] = data[field];
    }
  });

  return item;
};

After experimenting with Gridsome, Gatsby and Nuxt, it's a bit confusing not to have a function for handling data directly available from an import... But it's finally a good thing, since you better understand what it's hiding under the hood.

However, this React metaframework gave me the best development experience! In addition to having a complete routing system, Next also embeds the <Head /> component to enrich the page metadata of the application. Moreover, thanks to its <Image /> component (and not <img>), it offers a good optimization in the management of JPEG, PNG formats and... WebP, to get a better score on Google Lighthouse.

NB: Powered by Vercel, it's recommended to deploy its application on the service of the same name, to have a detailed overview of your website performance.

Where Next surprised me the most is during the compilation of the project (next build). Since version 12.0, the framework has improved its way of building its production release based on the Rust language, with the Speedy Web Compiler library (rather than Babel). This results in a considerable time saving (3 to 5 times faster than the previous version). I can only recommend to you!

The Wonderful World Of UnifiedJS #SpinOff

During this migration work, I took the time to discover what UnifiedJS really is. This ecosystem includes more than a hundred plugins to manipulate content. Whether it's <html>, .md/ .mdx formats or plain text, UnifiedJS's open-source libraries are able to browse each of these formats (using a tree syntax) and automate some tasks, such as syntax control, interpretation of code blocks, nodes transformation or minification.

This grouping includes:

  • RemarkJS, for processing Markdown files
  • RehypeJS, for processing HTML files
import { remark } from "remark";
import directive from "remark-directive";
import gist from "./remarkGist";
import gfm from "remark-gfm";
import html from "remark-html";
import prism from "remark-prism";

export const markdownToHtml = async (markdown) => {
  const result = await remark()
    .use(directive)
    .use(gist)
    .use(gfm)
    .use(html)
    .use(prism)
    .process(markdown);

  return result.toString();
};

In the example above, I use RemarkJS to transform the content of a .md file (##Hello, **World**) into HTML (<h2>Hello, <strong>World</strong></h2>). I also add support for enhanced GitHub syntax (GFM) to support tables and task lists. Finally, I use the Prism plugin to colorize code blocks (by language), according to a CSS theme.

import { visit } from 'unist-util-visit';

export default function remarkGist() {
  return (tree, file) => {
    visit(tree, (node) => {
      if (
        node.type === 'textDirective' ||
        node.type == 'leafDirective' ||
        node.type === 'containerDirective'
      ) {
        if (node.name !== 'github') return;

        const data = node.data || (node.data = {});
        const attributes = node.attributes || {};
        const id = attributes.id;

        if (node.type === 'textDirective') file.fail("Text directives for 'GitHub' not supported", node);
        if (!id) file.fail('Missing gist ID', node);

        data.hName = 'iframe';
        data.hProperties = {
          src: `https://gist.github.com/${id}`,
          width: 720,
          height: '100%',
          frameBorder: 0,
        };
      }
    });
  };
}

It's possible to develop your own transformation functions, to support video formats, or the addition of the Snippets from GitHub / GitLab, etc... Still in the example, I use a plugin allowing me to interpret directives, then I transform those corresponding to the ::github type by retrieving the Gist (from its identifier / URL) and embedding it in a <iframe> tag. With RehypeJS, I could also get the code (in RAW format) to pass it between <pre> and <code> tags. Everything is possible with UnifiedJS!

This "wonderful world" is supported by the JAMStack community, with contributors like Netlify, Vercel or Gastby. I strongly advise you to venture there (if not already done through "magic" plugins). Don't forget to equip yourself with your two best tools: RemarkJS et RehypeJS! πŸ§‘β€πŸ’»