Nuxt 3: Good to know

When switching my personal website to Nuxt 3 I found myself looking and researching all the small things you need to tackle, besides the content, to make it fly. Sitemap, robots.txt, proper page titles, include 3rd party JS and so on. While there is a lot written about a general setup of Nuxt, most blogs almost never talk about these small things.

You can find all of these tips in the repository codedge/codedge.de of this site.

Dynamic page title

Most websites you a similar page title like this:
<name of the current page><divider><general name of the website>

For example: Home | codedge. Let's create that.

Prerequisites:

In your app.vue you configure the central title content using the useHead configuration:

<script setup lang="ts">
useHead({
  htmlAttrs: {
    lang: "en",
  },
  titleTemplate: "%s | codedge",
})
</script>

This adds the general part, in my case | codedge, to every title for each page. To add the specific title for a page, open one of your *.vue files in the pages/ directory and add

<script setup lang="ts">
useHead({
  title: "My fancy title",
});
</script>

to it. This is going to build a title like My fancy title | codedge.

Conditionally load scripts

You might want to load scripts, f. ex. tracking scripts, depending on your environment. I wanted to include the Plausible tracking script only on the production environment.

In you app.vue you can do this:

<script setup lang="ts">
import { Script } from "@unhead/schema";

type HeaderScript = Array<Script>;
let scripts: HeaderScript = [];
let plausible: Script = {
  src: "https://plausible.io/js/script.js",
  defer: true,
  "data-domain": "<SET YOUR DOMAIN HERE>",
};

let env=process.env.NODE_ENV.toUpperCase()
// Only include the tracking script in the production environment
if (env === "PRODUCTION") {
  scripts.push(plausible);
}

useHead({
  script: scripts,
})

Make sure, you set your data-domain correctly according to your site.

Using images with Markdown

While including images in your repository might be a fast and straightforward solution you might want to consider using some image service like Cloudinary or Imgix as a central place for your images.

I chose Cloudinary which is supported by the NuxtImage plugin. After setting things up you might want to use this in your Markdown files. Using <nuxt-img> or <nuxt-picture> does unfortunately not work there. Wrapping this inside a Markdown Component (MDC) helps.

Create a file in components/content/Image.vue and add

<script setup lang="ts">
defineProps(["imgSrc"]);
</script>

<template><nuxt-picture provider="cloudinary" :src="imgSrc" /></template>

With that in place you can use NuxtImage in your Markdown files by writing

::image{imgSrc="<path/to/your/image.png>"}
::

A 404 component

You want to serve a custom 404 page, properly styled, whenever a visitor hits an unknown sub-page of your site. Instead of customizing the error.vue file, as suggested by other people, I went for writing a custom component which only deals with the 404 error.

Add a file components/NotFound.vue and add the following:

<script setup lang="ts">
const event = useRequestEvent();
setResponseStatus(event, 404);

useHead({
  title: "404",
});
</script>

<template>
  <div class="flex flex-col items-center justify-center px-5 mt-24">
    Ooops, page not found.
  </div>
</template>

Additionally to the content itself, we modify the page title and returning a proper 404 http status code.

I use this component in my pages/[...slug.vue] file (see here) file. So whenever a request is matched by this catchall route, deliver the 404 page, as probably no other content was found.

Sitemap

Currently neither Nuxt 3 or the Content module provides a way to generate a sitemap in the default installation. Luckily this is quick win to set up yourself.

First, install the NPM package sitemap by running yarn add -D sitemap.
Second, create a file server/routes/sitemap.xml.ts with the content

import { serverQueryContent } from "#content/server";
import { SitemapStream, streamToPromise } from "sitemap";

export default defineEventHandler(async (event) => {
    const docs = await serverQueryContent(event).find();
    const sitemap = new SitemapStream({
        hostname: "<YOUR FQDN, f. ex. https://www.codedge.de>",
    });

    for (const doc of docs) {
        sitemap.write({
            url: doc._path,
            changefreq: "monthly",
        });
    }
    sitemap.end();

    return streamToPromise(sitemap);
});

Don't forget to update the hostname in the code. You can now open your sitemap at https://your-domain/sitemap.xml.