Ultimate SEO Guide for Nuxt 3

Nuxt
SEO
Dec 26, 2022

A comprehensive guide to successful technical search engine optimization with Nuxt 3. Learn topics such as HTML basics, JSON-LD, OGP and Sitemap generation.

The content shown in this post is related to Nuxt 3. If you are looking for SEO with Nuxt 2, you can check out the post Simple steps for successful SEO with Nuxt 2.

Basics

Title & Description

Setting the title and description of websites is considered one of the fundamental measures for SEO. These elements play a crucial role as they are displayed in search engine results and greatly influence a visitor's decision to click and visit your website. By carefully crafting compelling titles and descriptions, you can effectively communicate the value and relevance of your content, increasing the likelihood of attracting and engaging visitors.

To set the title and description of a page, Nuxt 3 has the Composable useHead, which can be called inside <script setup> of every page and component to manage your head tags.

Vue
pages/index.vue
<script setup lang="ts">
  useHead({
    title: 'Nuxt: The Intuitive Web Framework',
    meta: [
      {
        name: 'description',
        content: 'Build your next Vue.js application with confidence using Nuxt....' 
      },
    ],
  });
</script>

Alternatively, Nuxt 3 also provides meta components such as <Title /> or <Meta />, which can be used as an alternative to the useHead Composable. These components operate at the template level and need to be wrapped around the <Head /> component.

Vue
pages/index.vue
<template>
  <Head>
    <Title>Nuxt: The Intuitive Web Framework</Title>
    <Meta
      name="description"
      content="Build your next Vue.js application with confidence using Nuxt...."
    />
  </Head>
</template>

Take a look at the list of all available meta components to learn more about them.

Moving forward, our sole focus will be on utilizing useHead.

Update: Nuxt has come up with a new composable useSeoMeta, which is smart way to set up fundamental SEO metadata. It helps you avoid mistakes while using useHead. You can check out useSeoMeta, but we will continue using useHead.

Language

It is essential to consistently indicate the language of your website. You have two options to achieve this. Firstly, if your site is exclusively in one language, you can set the language globally in the app.vue file. Alternatively, you can continue specifying the language on a per-page basis. In both cases, the useHead method can be employed to accomplish this task.

Vue
app.vue
<script setup lang="ts">
  useHead({
    htmlAttrs: { lang: 'en' },
  });
</script>

Canonical

The canonical URL describes where the original content of your page is located at and should be set for each page. Search engines will consider the canonical URL with increased relevance.

Vue
pages/index.vue
<script setup lang="ts">
  useHead({
    link: [{ rel: 'canonical', href: 'https://nuxt.com/' }],
  });
</script>

Open Graph protocol (OGP)

Specifying metadata for social media plays a vital role as it enables the generation of comprehensive preview cards when sharing your pages. In this example, we will focus on implementing Facebook's Open Graph protocol (OGP) to achieve this.

The Open Graph protocol enables any web page to become a rich object in a social graph. For instance, this is used on Facebook to allow any web page to have the same functionality as any other object on Facebook.

The Open Graph protocol

test

It is recommended to provide essential metadata such as the title, type, image, and URL of the page when implementing social media preview cards. Additionally, you have the option to specify other properties such as a description or the language. For more in depth information, read further on Open Graph protocol.

Vue
<script setup lang="ts">
  useHead({
    meta: [
      { property: 'og:title', content: 'The Intuitive Web Framework' },
      { property: 'og:description', content: 'Build your next Vue.js application with confidence using Nuxt...' },
      { property: 'og:type', content: 'website' },
      { property: 'og:url', content: 'https://nuxt.com' },
      { property: 'og:locale', content: 'en_US' },
      { property: 'og:image', content: 'https://nuxt.com/social.jpg' },
  });
</script>

These details will result in a preview with title, description and image when shared on social media, as shown in the image below.

Structured data (JSON-LD)

JSON-LD is a widely adopted format for creating structured data. It provides a standardized way to organize and present data on your website. For instance, you can utilize JSON-LD to store FAQ elements as structured data. When implemented correctly, these FAQ elements can appear in search results as a distinct questions and answers section, enhancing the visibility and usability of your content.

Setup

We will use the Nuxt module nuxt-jsonld. First, install the dependency.

bash
NPM
npm install nuxt-jsonld

Now add it to your modules inside your Nuxt configuration.

TypeScript
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-jsonld'],
});

Implementation

Now we have the composable useJsonld() available within <script setup>, allowing us to specify any structured data. In the following we will add a single FAQ element to the page as structured data.

Vue
pages/index.vue
<script setup lang="ts">
  useJsonld({
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    mainEntity: [
      {
        '@type': 'Question',
        name: 'Lorem ipsum dolor sit amet?',
        acceptedAnswer: {
          '@type': 'Answer',
          text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr.',
        },
      },
    ],
  });
</script>

You can use the playground of json-ld.org to try out some examples. Also, you can test your structured data at any time with Test Rich Results. On Schma.org, you can see all the different types and parameters that are available to use.

HTML

Headline hierarchy

Search engines try to understand your website. It is advisable to establish a well-structured heading structure. For example, each page should only have one <h1> element. The headings should be logically structured according to the content.

But you don't have to check the structure manually in your HTML. There are tools for that. If you use Google Chrome, I can highly recommend META SEO inspector.

Links

When creating links, it is important to include both the link text and the relation attribute, such as rel="external". Search engines rely on this information to better understand the purpose and nature of the links. If you have a link that only displays an icon, you can still include a hidden text using CSS. It is essential to provide this information for all links on your website.

For a comprehensive list of possible rel attribute values and their meanings, you can refer to the MDN article on this topic. The article will provide you with valuable insights into the available options for specifying the relationship between your links and the linked resources.

Vue
<template>
  <!-- INTERNAL LINK -->
  <NuxtLink to="/" rel="next">
    <Logo />
    <!-- TEXT HIDDEN WITH CSS -->
    <span class="sr-only">Homepage</span>
  </NuxtLink>
  <!-- EXTERNAL LINK -->
  <NuxtLink to="https://nuxt.com/" rel="external">
    Nuxt
  </NuxtLink>
</template>

Images

When it comes to images, similar principles apply as with links, but with the usage of different attributes. It is essential to consider the following guidelines:

  • Alternative Text: Each image should have descriptive alternative text that conveys the meaning of the image. The alt attribute is used for this purpose and is crucial for accessibility and search engine understanding.

  • Loading: The loading attribute specifies when an image should load. For images located far outside the visible area, you can use the value "lazy." This means the image will only load as it approaches the viewport. If you want the image to load immediately, you can specify the value "eager."

  • Width and Height: Always include the width and height attributes for images to avoid layout shifts (Cumulative Layout Shift - CLS). When the dimensions are specified, it helps the browser allocate the necessary space for the image, preventing unexpected layout changes. Search engines, including Google, consider a good user experience and stable layout as important ranking factors.

  • Title: The title attribute can provide additional context and information about the image. It serves both accessibility purposes and aids search engines in understanding the image's content.

Vue
<template>
  <img
    src="https://placeholder.pics/svg/640x480"
    alt="Placeholder"
    loading="lazy"
    width="640"
    height="480"
    title="Placeholder"
  />
</template>

We will soon be covering the topic of optimized images with Nuxt Image. Stay tuned!

Favicon

Your website should also show your logo in the browser tab. For this purpose, there is a really handy tool called realfavicongenerator. You can upload your logo, and it will help you convert it to favicons. After that, you can download the favicons and paste them into your /public directory.

The only thing left for you to do is to include the favicons globally. For this, we will use app.vue in this example to set the favicon globally.

This is how it could look like:

Vue
app.vue
<script setup lang="ts">
  useHead({
    link: [
      { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' },
      { rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' },
    ],
  });
</script>

Sitemap

Every website should have a sitemap. The sitemap allows your website to be indexed by search engines. It includes all the subpages of the website, but also can include pages of particular importance, as well as language versions of a page. Mostly, the sitemap is a file called sitemap.xml and is placed in the root directory of your webspace. For example, you can check this website's sitemap here: https://thenextbit.de/sitemap.xml.

Given that the community module @nuxt/sitemap hasn't made the transition to Nuxt 3 yet, you can consider creating the sitemap yourself. In the following, I will show you two ways to accomplish this.

Custom Sitemap Module for SSG

If you are new to working with custom modules in Nuxt 3, I highly recommend referring to the comprehensive Module Author Guide to gain a deeper understanding.

The purpose of this module is to seamlessly integrate into the build process and automatically generate a sitemap that dynamically contains the pages from your CMS. This is made possible by leveraging the nitro:build:public-assets hook, which conveniently triggers just before the SSG's build process begins.

To get started, we need to install the xml dependency. It is a sophisticated JavaScript-based XML generator that will aid us in creating the sitemap in the correct xml-format.

bash
npm install xml -D

In this example, we will name the module sitemap.module.ts and place it directly within the ~/modules folder.

Within this file, retrieve your page data from the appropriate source. Then, configure the response parameters to match the structure required by your API.

TypeScript
sitemap.module.ts
import { writeFileSync } from 'fs';
import { defineNuxtModule } from '@nuxt/kit';
import xml from 'xml';

export default defineNuxtModule({
  meta: {
    name: 'sitemap',
    configKey: 'sitemap',
  },

  defaults: {
    declaration: '<?xml version="1.0" encoding="UTF-8"?>',
    xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9',
  },

  setup(options, nuxt) {
    nuxt.hook('nitro:build:public-assets', (nitroConfig) => {
      // SKIP ON DEVELOPMENT
      if (nitroConfig.options.dev) return;

      // GET YOUR PAGES FROM YOUR DATA SOURCE
      const pages = [
        {
          loc: 'https://nuxt.com/',
          lastmod: '2023-11-01',
          priority: 1.0,
          changefreq: 'monthly',
        },
      ];

      // ADJUST ACCORDING TO YOUR DATA SOURCE
      const sitemapItems = pages.map(({ loc, lastmod, priority, changefreq }) => {
        return {
          url: [{ loc }, { lastmod }, { priority }, { changefreq }],
        };
      });

      // CREATE SITEMAP
      const sitemap =
        options.declaration +
        xml({
          urlset: [{ _attr: { xmlns: options.xmlns } }, ...sitemapItems],
        });

      // WRITE SITEMAP
      writeFileSync(nuxt.options.rootDir + '/.output/public/sitemap.xml', sitemap);
    });
  },
});

In order for the module to be picked up by Nuxt, we still need to add it to the Nuxt configuration.

TypeScript
nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '~/modules/sitemap.module',
  ],
});

Now, the sitemap module is fully prepared and operational, ready when generating your site with npm run generate.

To further optimize this module, consider implementing additional enhancements. For instance, you can output the relevant steps in the console. This will provide clarity and transparency, allowing you to easily track and comprehend the progression of each step while generating your site.

Take this as a starting template and customize it for your individual needs.

Sitemap with Server Routes

In this section, I'll demonstrate how to create a server-side generated sitemap that will be created on request. Similar to the example on creating a custom sitemap module for SSG above, we’ll use the xml dependency for this setup.

bash
npm install xml -D

Start by setting up a server route in ~/server/routes/sitemap.xml.ts. Within this file, retrieve your page data from the appropriate source. Then, configure the response parameters to match the structure required by your API.

TypeScript
sitemap.xml.ts
import xml from 'xml';

export default defineEventHandler((event) => {
  // GET YOUR PAGES FROM YOUR DATA SOURCE
  const pages = [
    { loc: 'https://nuxt.com/', lastmod: '2023-11-01', priority: 1.0, changefreq: 'monthly' },
  ];

  // CREATE SITEMAP ITEMS
  const sitemapItems = pages.map((page) => {
    return {
      url: [
        { loc: page.loc },
        { lastmod: page.lastmod },
        { priority: page.priority },
        { changefreq: page.changefreq },
      ],
    };
  });

  // CREATE SITEMAP
  const sitemap =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    xml({
      urlset: [
        { _attr: { xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9' } },
        ...sitemapItems,
      ],
    });

  // SET RESPONSE HEADER
  setHeader(event, 'Content-Type', 'text/xml');

  // RETURN SITEMAP
  return sitemap;
});

Once set up, your sitemap will be accessible at /sitemap.xml on your domain. Use this as a foundation and tailor it to suit your specific needs.

Feedback

Thanks for reading! If you have any feedback, please feel free to email us to info@thenextbit.de. We would be happy to hear from you.