NUXT NATION 2022 NOTES

Nuxt Nation 2022 Notes

November 27, 2022 . Mostafa Lotfy

Nuxt Nation

Nuxt Nation is an online event that is held by Vue School, with speakers such as the Nuxt and Vue authors.

These are some notes I took while attending the online conference on 16 & 17 November 2022.

Some of the talks, that I took notes from can be found on the Vue School YT channel

There are Free workshops from Nuxt Nation, with limited seats.

Nuxt 3.0 Stable

Nuxt 2 vs Nuxt 3

Nuxt 3 javaScript size vs Nuxt 2

Nuxt 3 is fast, it has Hybrid rendering, and SEO and web vitals

Client Side Error Handing In Nuxt

<template>
	<lessonCompleteButton
		:model-value="isLessonComplete"
		@update:model-value="
			throw createError('could not update');	
		"
	/>
</template>
<template>
	<NuxtErrorBoundry @error="LogSomeError">
		<!-- wrap it around the parent component that holds the component with the createError method -->
		<NuxtPage />
		<template #error="{ error }">
			<p>
				Oh no, something broke!
				<code>{{ error }}</code>
			</p>
		</template>
	</NuxtErrorBoundry>
</template>
<template>
	<NuxtErrorBoundry @error="LogSomeError">
		<!-- wrap it around the parent component that holds the component with the createError method -->
		<NuxtPage />
		<template #error="{ error }">
			<p>
				Oh no, something broke!
				<code>{{ error }}</code>
			</p>
			<p>
				<button
					@click="resetError(error)"
				>
					Reset
				</button>
			</p>
		</template>
	</NuxtErrorBoundry>
</template>
 
<script setup>
const resetError = (error) => {
	error.value = null
}
</script>
<template>
	<NuxtErrorBoundry @error="LogSomeError">
		<!-- wrap it around the parent component that holds the component with the createError method -->
		<NuxtPage />
		<template #error="{ error }">
			<p>
				Oh no, something broke!
				<code>{{ error }}</code>
			</p>
			<p>
				<button
					@click="resetError(error)"
				>
					Reset
				</button>
			</p>
		</template>
	</NuxtErrorBoundry>
</template>
 
<script setup>
const resetError = async (error) => {
	await navigateTo(
		'/course/chapter/1-chapter-1/lesson/...'	
	)
	error.value = null
}
</script>

Common errors

createError method has the option to create a fatal error. If it's an error we can't recover from you can use it to though a full-screen error.

error monitoring systems

NuxtErrorBoundry Vs v-if

If you navigate away without resetting the error the error will still be there. Then it's best to do some sort of middleware that resets errors or perhaps a key on the NuxtErrorBoundry that is key to the route that you're on so each time you navigate away it forces it to reset the error.

The error can return an object as well as a message.

Vue Js Composables

<!-- /pages/about.vue -->
 
<template>
	<!-- ... -->
</template>
 
<script setup>
const title = 'About'
const description = '...'
 
useSimpleHead({ title, description }) // You can just add this line to all your pages.
 
</script>
<!-- /composables/useSimpleHead.ts -->

export default ({ title, description }) => {
	useHead({
		title,
		meta: [
			{ hid: "description", name: "description", content: description },
			// Open Graph
			{ hid: "og:title", name: "og:title", content: title },
			{ hid: "og:description", name: "og:description", content: description },
			// Twitter
			...
		]
	})
}

Nuxt Image

https://v1.image.nuxtjs.org

install yarn add --dev @nuxt-image-edge

A package that allows you to resize and transform your images using a built-in optimizer or your favorite image CDN

add the model in the nuxt.config.ts file

export default defineNuxtConfig({
	modules: [
		'@nuxtjs/tailwindcss',
		'@nuxt/image-edge',	
	]
})

Use <nuxt-img> tag instead of the <img> tag inside your components

<!-- app.vue -->
 
<template>
	<div class="...">
		<nuxt-img 
			src="/images/my-image.jpeg"
			width="750"
			height="350"
			format="avif"	
		>
		<nuxt-picture
	</div>
</template>

You can also use the <nuxt-picture> tag

<!-- app.vue -->
 
<template>
	<div class="...">
		<nuxt-image
			src="images/my-image.jpeg"
			
		>
	</div>
</template>

Vue Macros

defineModel code example

defineModel with reactivity transform code example

macro vs runtime code a macro allows more complicated features to be implemented.

Nuxt Rendering Modes

Nuxt rendering docs

Nuxt Custom Dev Tools

use-cases

What are feature flags?

can be used for beta testing

if (featureFlag['newFeature'] === true) {
	renderNewFeature()
	changeAppBehavior
}
}

can be used for a/b testing

if (featureFlag['experiment-A'] === true) {
	buttonCssClass = 'text-green'
}

deploy without risks

if (featureFlag['risky-feature'] === true) {
	compute()
}

Roll back without code changes

if (featureFlag['it-wasnt-me'] === true) {
	throw new Error('opps!')
}

Test performance profiling

if (featureFlag['test-pref'] === true) {
	await newQuery()
} else {
	await oldQuery()
}

Kill switches

// For example disable some features on a given day
if (featureFlag['kill-feature'] === true) {
	compute()
}
if (featureFlag['goodbye-feature'] === false) {
	continueWithFeature()
}

npx nuxi init -t module my-module

@nuxt/kit A library that encapsulates all the methods you'll need to create Nuxt modules.

export default defineNuxtModule<ModuleOptions>({
	meta: {
		name: "module-name",
		configKey: "module-key-in-nuxt-config"
	},
	defaults: {
		// default module config
	},
	setup(options, nuxt) {
		// add plugins, components, etc	
	}
})

How Nuxt 3 is allowing us to build faster apps

As page load speed increases conversion rate decreases

Core web vitals: LCP (largest contentful paint), FID (first input delay), and CLS (cumulative layout shifts)

What makes a good performance?

Metrics from builder.io

Simple to do app speed comparison across different front end frameworks

Complex app speeds comparison across different frontend frameworks

Benchmarks are useful but they are not the ultimate source of truths. So many things that can influence benchmarks.

CLS (Cumulative Layout Shifts)

Things that can cause layout shifts

To improve LCP and FID

<NuxtLink to="path" prefetch>
	This link will be prefetched, when it appears in the viewport
</NuxtLink>

Nuxt on the Edge

Different hosting providers (including cloudflare, netlify, and Vercel) that Nitro works with

Comparing Nuxt 2 to Nuxt 3

Vue 3 main features; increased performance, TypeSCript support, and it works with composition API

Nuxt 3 has better performance, improved SEO, and allows for fast static site generation

Nuxt 3 runs anywhere, built for fast cold starts, and supports hybrid rendering

Hybrid Rendering in Nuxt.js 3

Zero-config Nuxt deploys on different platforms including Cloudflare pages, Vercel, and Netlify

Nuxt 2 used Webpack and CLI tooling

Nuxt 3 supports Vite and Nuxi CLI tooling

File structure

definePageMeta({
	middleware: ["auth"],
	pageTransition: 'fade',
	layout: 'blog',
})	
useHead({
	title: "Hello World"
})

Nuxt 3 components directory

Nuxt 3 render page must be wrapped and uses slot element

	// middleware/log.js
export default defineNuxtRouteMiddleware((to, from) => {
	return navigateTo('/login') // a redirect
})	

Multi-Variant Apps With Nuxt3

// A simple example
// single base app, e.g. a theme
export default defineNuxtConfig({
	extends: 'content-wind' // a tailwind starter template
})
// Use multiple sources
export default defineNuxtConfig({
	extends: [
		'../base-app',
		'github:manniL/nuxt-extends-test',
		'../feature-ecommerce-shopify',
		'../feature-tracking-plausible'	
	]
})

app.config.{ts,js}

export default defineAppConfig({
	socials: {
		twitter: 'TheAlexLichter'	
	}
})
<script setup lang='ts'>
	const appConfig = useAppConfig()
	console.log(appConfig.socials.twitter) // 'TheAlexLichter'
</script>

Caveats

Upcoming features

Building and deploying mobile apps with Nuxt/Ionic

Why Nuxt/Ionic?

Nuxt Ionic Features

What is Capacitor

Getting Started with Nuxt/Ionic

  1. Create a starter Nuxt 3 app

npx nuxi dev

  1. Add Nuxt/Ionic Module

npm install @nuxtjs/ionic -D

// /nuxt.config.ts
 
export default defineNuxtConfig({
	target: "static",
	modules: ['@nuxtjs/ionic']
})
// /app.vue
 
<!--ion-router-outlet allows you to use file-based routing, but gives you the look and feel as you navigate your mobile app-->
 
<template>
	<ion-app>
		<ion-router-outlet />
	</ion-app>
</template>
  1. Set up routing
  1. Use components & Icons
Check the demo's GitHub repo
<!-- index.vue -->
 
<template>
	<!-- ion-page is the root component of any page in an ionic app -->
  <ion-page>
    <ion-header :translucent="true">
      <ion-toolbar>
        <ion-title>Nuxt Ionic</ion-title>
      </ion-toolbar>
    </ion-header>
 
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">Nuxt Ionic</ion-title>
        </ion-toolbar>
      </ion-header>
      <div id="container">
        <h1>Hello Nuxt Nation 👋!</h1>
 
        <ion-button router-link="/resources">Get Started</ion-button>
      </div>
    </ion-content>
  </ion-page>
</template>
 
<style scoped>
#container {
  text-align: center;
 
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
}
#container strong {
  font-size: 20px;
  line-height: 26px;
}
#container p {
  font-size: 16px;
  line-height: 22px;
 
  color: #8c8c8c;
 
  margin: 0;
}
#container a {
  text-decoration: none;
}
</style>
 
<!-- No script tag as we don't need to import anything unlike using ionic/vue -->
<!-- /pages/resouces.vue -->
 
<template>
  <ion-page>
    <ion-header :translucent="true">
      <ion-toolbar>
        <ion-title>Resources</ion-title>
      </ion-toolbar>
    </ion-header>
 
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">Resources</ion-title>
        </ion-toolbar>
      </ion-header>
      <div id="container">
        <ion-list :inset="true">
          <ion-item>
            <ion-icon :icon="ioniconsBookOutline" slot="start"></ion-icon>
            <ion-label
              ><a
                target="_blank"
                rel="noopener noreferrer"
                href="https://ionic.nuxtjs.org/"
              >
                Nuxt Ionic Documentation</a
              ></ion-label
            >
          </ion-item>
          <ion-item>
<!-- ionic icons are auto imported :icon="ionicons*", start by ionicons then add the icon's name -->
            <ion-icon :icon="ioniconsRocketOutline" slot="start"></ion-icon>
            <ion-label>
              <a
                target="_blank"
                rel="noopener noreferrer"
                href="https://stackblitz.com/github/nuxt-modules/ionic/tree/main/playground"
                >Nuxt Ionic Playground</a
              ></ion-label
            >
          </ion-item>
          <ion-item>
            <ion-icon :icon="ioniconsLogoGithub" slot="start"></ion-icon>
            <ion-label>
              <a
                target="_blank"
                rel="noopener noreferrer"
                href="https://github.com/nuxt-modules/ionic"
                >Nuxt Ionic GitHub Repo</a
              ></ion-label
            >
          </ion-item>
          <ion-item>
            <ion-icon :icon="ioniconsVideocamOutline" slot="start"></ion-icon>
            <ion-label class="ion-text-wrap">
              <a
                target="_blank"
                rel="noopener noreferrer"
                href="https://www.youtube.com/watch?v=f4sB7NhCgRw"
                >Video: Getting Started with Nuxt Ionic Module for Nuxt 3 by
                Aaron Saunders</a
              ></ion-label
            >
          </ion-item>
        </ion-list>
        <ion-button router-link="/">Back</ion-button>
      </div>
    </ion-content>
  </ion-page>
</template>
 
<style scoped>
#container {
  text-align: center;
 
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
}
#container strong {
  font-size: 20px;
  line-height: 26px;
}
#container p {
  font-size: 16px;
  line-height: 22px;
 
  color: #8c8c8c;
 
  margin: 0;
}
#container a {
  text-decoration: none;
}
</style>
  1. Update the theme

paste the CSS variables code

/* ... */
export default defineNuxtConfig({
	target: "static",
	modules: ['@nuxtjs/ionic'],
	css: ["@/theme/variables.css"]
});

Adding Capacitor to your Nuxt/Ionic project

  1. Install Ionic CLI

npm install -g @ionic/cli

ionic integrations enable capacitor

  1. Add an android or IOS project ionic capacitor add android

  2. Build web code we need to sync the android project with a web build version of our app.

  1. Sync project with capacitor.

npx cap sync -- This will copy the build to our android project

  1. Open and run on mobile

npx cap run android pick an emulator It should open up and run

  1. For IOS The process is similar for IOS. You need XCode and Xcode CLI installed Then replace any command that had android in it with IOS

Deploying on Appflow

A cloud app management system built by ionic a paid service

main features

Ionic Resources

Good solutions for working with Icons in Nuxt

Why use Nuxt when you can do it yourself with Vite? (Nuxt Vs. Vite functionalities differences)

How do you balance your primary job with maintaining open-source projects?

Does npx nuxi generate scale? Is there a plan for an ISR (incremental static regeneration)?

Are there any other big meta frameworks like Nuxt that use Vite?

Which is better to create a large app Next.js or Nuxt.js?

Micro-frontend Architecture in Nuxt 3?

Can you talk about clean architecture and why we should or should not apply it to front-end development?

How to use concurrency and interval properties in NuxtJs static mode, when there are many headless cams data API calls on hundreds of pages?

What is the process of becoming a Nuxt ambassador?

What are the next big steps for Nuxt, Vite, and Vue?

Nuxt In Large Companies

Nuxt as a backbone of a scalable platform

JUMBO Showcase

Well-known offline and online grocery chains in the Netherlands and Belgium. They develop a lot of their software in-house. Over 300 devs working First, they grabbed an off-the-shelf solution. They started to add a couple of Vue components. They grew and had different teams to specifically handle different customer journeys. They switched from a monolith to a distributed application architecture

Jumbo adding Vue to the stack

As the system evolved the complexities increased.

They were using Vue and their apps were using Vue components.

So they build a component library.

Jumbo using Vue, CMS, and component library to handle apps

Jumbo added nuxt to the stack

A lot of maintenance for all these micro apps, and for the component library

What they wanted instead

They considered using Nuxt as a simple app that would connect all these modules. They discarded this idea because they didn't want people to be working on the same codebase, depending on other teams' features to be delivered to deliver theirs.

Instead, they built their system

  1. Built a component library.
  1. CMS
  1. Content Service
  1. Content Renderer

Component library pointing to CMS

Headless CMS connected to the adapter. An adapter is connected to the content service. And Content service connected to a database.

customer with an arrow pointed to nuxt. Nuxt pointed to CMS. And CMS pointing to database

Challenges

It works for them. But they do use caching.

Which CMS does JUMBO use?

Nuxt Extends

How it works in code.

// ./package.json
 
{
	"private": true,
	"scripts": {
		"build": "nuxt build",
		"dev": "nuxt dev",
		"generate": "nuxt generate",
		"preview": "nuxt preview"	
	},
	"devDependencies": {
		"@nuxtjs/tailwindcss": "^6.1.0",
		"nuxt": "3.0.0-rc.12"
	},
	"dependencies": {
		// This will be used to extend our app
		"nuxt-commerce-theme": "^0.0.1"	
	}
}
// ./nuxt.config.ts
 
export default defineNuxtConfig({
	modules: ['@nuxtjs/tailwindcss'],
	extends: './node_modules/nuxt-commerce-theme'
})

Inspecting the "nuxt-commerce-theme" in the node-modules. You'll see that this package comes with the nuxt.config.ts file and a vue components folder that you can use in your app.

So now we can add these components to our Nuxt app.

<!-- ./app.vue -->
<!-- All these components we haven't developed in our app, but were extends from another app -->
 
<template>
	<div>
		<HeroBanner />
		<TheFooter />
	</div>
</template>

So Nuxt extends to allow you to build reusable Nuxt themes.

Pinceau

piceau

End-to-End Type Safety with Nuxt and tRPC

What is type safety?

Is when the compiler validates types while compiling, throwing an error if you assign the wrong type to a variable.

It just reduces a handful of bugs that you might not see otherwise.

What is end-to-end type safety?

End-to-end type safety is having a single source of truth for your types across all the layers of your app.

If the User object changes for one of your layers, for the database for example, then it is going to require a lot of maintainabilities and more work. Can create issues, headaches, and a lot of work.

Type Script Remote Procedure Call (tRPC)

How to use tRPC in a Nuxt application

// nuxt.config.ts
export default defineNuxtConfig({
	modules: ['trpc-nuxt/module'],
	typescript: {
		strict: true,	
	}
})
// server/trpc/trpc.ts
import { initTRPC } from '@trpc/server'
 
// Create the t-object and extract the helpers you plan on using
// Options include
	// Router
	// Procedure
	// Middleware
 
const t = initTRPC.create()
 
// Base router and procedure helpers
 
// Router provides a space to collect related procedures with a shared namespace
export const router = t.router
// Procedure can be viewed as rest endpoints that first validate the input using Zod, Yup, or Superstruct
export const publicProcedure = t.procedure
// server/trpc/routers/helloRouter.ts
 
// Import the router and procedure created in server/trpc/trpc.ts
import { z } from 'zod'
import { publicProcedure, router } from '../trpc'
 
// Call your router function and define your first procedure. You can think of this as an HTTP endpoint `apic/trpc/hello`
export const appRouter = router({
  hello: publicProcedure
	// Validate the input using Zod. Here we are defining an input parameter named text that expects a string.
    .input(
      z.object({
        text: z.string(),
      }),
    )
// .query() is the same as an HTTP GET request. Here it is taking the input defined above and returning something based on that input.
    .query(({ input }) => {
      return {
        greeting: `hello ${input?.text ?? 'world'}`,
      }
    }),
})

Another example; an Auth Router with a login procedure.

// server/trpc/routers/authRouter.ts
 
// Import the router and procedure created in server/trpc/trpc.ts
import { z } from 'zod'
import { publicProcedure, router } from '../trpc'
 
export const appRouter = router({
  hello: publicProcedure
	// Validate the input using Zod. Here we are defining an input parameter named text that expects a string.
    .input(
      z.object({
        text: z.string(),
      }),
    )
// We're calling the `.mutation()` function which is the equivalent of an HTTP POST request.
	.mutation(({ input }) => {
		// Here some login stuff would happen
		return {
			user: {
				name: input.name,
				role: 'ADMIN',
			},
		}
	}),
})

This is the entry point for your API and exposes the tRPC router. This is where you can expose your API to middleware to manage things like CORS.

// server/api/[trpc].ts
import { createNuxtApiHandler } from 'trpc-nuxt'
import { appRouter } from '@/server/trpc/routers'
 
// export API handler
export default createNuxtApiHandler({
  router: appRouter,
  createContext: () => ({}),
})

Merging the routers into a centralized appRouter

// server/trpc/routers.ts
 
import { router } from '../trpc'
import { authRouter } from './authRouter'
import { helloRouter } from './helloRouter'
 
const appRouter = router({
	auth: authRouter,
	hello: helloRouter,
})
 
// export type definition API
export type AppRouter = typeof appRouter

The AppRouter type signature is passed to trpc-nuxt's createTRPCNuxtPorxyClient function.

Using provide to inject trpc into the client.

// plugins/trpc.ts
 
import { httpBatchLink, createTRPCProxyClient } from '@trpc/client'
import type { AppRouter } from '@/server/trpc/routers'
 
export default defineNuxtPlugin(() => {
  const trpc = createTRPCProxyClient<AppRouter>()
  return {
    provide: {
      trpc,
    },
  }
})
 

Using it will give you autocompletion on the client side. Clicking on the query in the index.vue file (fort end) takes you directly to your back end (helloRouter.ts) Refactoring queries and procedures. Changing them on the client site will change them automatically on the backend.

<!--index.vue-->
 
<script setup lang="ts">
const { $trpc } = useNuxtApp()
 
// query and mutate use useAsycData under the hood
const { data, pending, error } = await $trpc.hello.hello.query({ greeting: 'Nuxt Nation!' })
</script>
 
<template>
	<div v-if="pending">...loading</div>
	<div v-else-if="error?.data?.code">Error: {{ error.data.code }}</div>
	<div class="container" v-else>
		<p class="text">{{ data?.greeting }}</p>
	</div>
</template>
 

Does tRPC provide runtime validation or is it all about types for use at development time?

Is trpc-nuxt ready for production? Not yet.

Add 3D to your Nuxt app with Three.js

First, we need to draw something

const sphere = new Three.mech(
	// Geometry
	new THREE.SphereGeometry(1, 32, 32),
	// Material
	new THREE.MeshBasicMaterial({
		color: 0*008080	
	})
)

Then we need a camera to see it

ThreeJs has 2 types of cameras. We'll use perspective because it's the most similar to the human eye.

const aspectRatio = 
	  window.innerWidth /
	  window.innerHeight
 
const camera = 
	  new THREE.PerspectiveCamera(
		 45, // FOV
		 aspectRation, 
		 0.1, // Near plane
		 1000 // Far plane 
	  )
 
camera.position.set(4, 4, 4)

A scene to put it all together

const scene = new THREE.Scene()
 
// Adding our camera to the scene
scene.add(camera)
 
// Adding the sphere
scene.add(sphere)

A renderer to show it

const renderer = new THREE.WebGLRenderer({
	canvas
})
 
renderer.setSize(
	window.innerWidth,
	window.innerHeight,
);
 
renderer.setPixelRatio(Math.min(window.devicePixelRatio))
 
renderer.render( scene, camera )
<script setup>
// ThreeJs related code ...
</script>
 
<template>
	<canvas />
</template>

This won't work cause we didn't pass the reference for these canvas elements

Use TemplateRefs + Lifecycle hooks

<script setup lang="ts">
const experience: Ref<HTMLCanvasElement | null> = ref(null)
let renderer: WebGLRenderer
 
onMounted(() => {
	renderer = new THREE.WebGLRenderer({
		canvas: experience.value	
	})
 
	...
})
</script>
 
<template>
	<canvas ref="experience" />
</template>

It renders but it looks like an image. No interaction.

This is because we only render it once.

The Loop

The requestAnimationFrame function allows us to create a loop that runs at the browser's refresh rate.

const loop = () => {
	
	// Move object 
	sphere.position.x += 0.01
 
	renderer.render( scene, camera );
	requestAnimationFrame(loop)
}
 
loop()

We have a sphere that animates smoothly.

Be mindful of reactivity

Nuxt + ThreeJS

The 3D world is not optimal for SSR, it might work but performance will not be optimal.

Nuxt allows you to use the ClientOnly component to render a component only on the client side.

<template>
	<ClientOnly>
		<TheExperience />
	</ClientOnly>
</template>

Or you can add the .client suffix to the component file

Next steps

Resources

Any performance gotcha's that are important to watch out for when using ThreeJs with Nuxt?

UNJS and Nuxt 3

Presentation link

unified Javascript tools.

unjs/unbuild

Made for building Nuxt 3 modules and packages

unjs/changelogen

Beautiful change logs using conventional commits Used for Nuxt 3 releases notes and change-logs

unjs/unplugin Unified plugin system for Vite, Rollup, Webpack, and more.

unjs/unimport A system that makes imports automated. Smartly tree shaken. ...

unjs/ofetch

Better fetchAPI natively works on node, browsers, and workers. Provides universal $fetch for Nuxt 3.

node-fetch-native If you have a custom server or nodejs project. Future proof. Supports native fetch implementations for the server.

unjs/h3

Minimal h(ttp) framework build for high performance, composition, and portability Makes it easier to scale up the project. Fast. You can use h3 for any custom solution you have. Nitro is built on h3.

unjs/unstorage Universal storage layer. Cross-platform, Multi Driver KV storage powering Nuxt 3 and Nitro cache. Persistent storage is one of the essential parts of making a universal framework. You can combine different stores into a simplified key and value storage layer. Used by Nuxt for caching and deployment Works out of the box for any deployment platform

unjs/listhen

unjs/c12

unjs/ipx

unjs/untyped

unjs/giget

unjs/nitro

What is headless?

Example app using Storyblok and Algolia with Nuxt

Stack

Nuxt Security

yarn add nuxt-security

Security module for Nuxt based on OWASP Top 10 and Helmet Just by importing this module, you're making your app a bit more secure.

Supercharged <head> management

Vue head v1 and the future of SEO with Nuxt v3

HTML Metadata, Meta Info, Meta tags, Head tags

All the same. Managing machine-readable elements and attributes outside the Vue render tree for SSR and DOM.

VueUse Head useHead composable

<script setup>
useHead({
	title: 'My page title',
	meta: [
		{ name: 'description', content: 'My description'}	
	]
})
</script>

What does being a VueUse composable mean?

Accepts reactive arguments: ref, computed, and reactive getter.

<script setup>
const title = ref('My page title')
useHead({
	title
})
title.value = 'My new title'
</script>

Cleans up the side effect automatically, if it has any.

Avoid wrapping in watchers.

// Bad
watch(ref, ()=>{
	useHead({title:ref.value})
})
 
// Good
useHead({title: ()=> ref.value})

What's going on with useHead

Making SEO Easier

useSeoMeta code example

Nuxt-unhead get early access to experimental features at github.com/harlan-zw/nuxt-unhead

Practical Nuxt 3 SEO Tips

Performance

SEO

Do these meta tags have to be rendered server-side or will crawlers still find them when they're rendered on the client side?

Rendering on the server side is a safe option. Google said we're going to run your JS, so it might be fine. However, server rendering them is highly recommended. No reason not to, unless you have a single-page app (SPA), in those instances Nuxt 3 will read your head config and throw that in the SPA's HTML template.

Can useHead be used in custom composable?

useHead can be used anywhere, it's kind of like Pinia, it has a global state that we can reference rather than relying on the specific Vue setup state. If you want to use useHead in middleware, custom composable go for it. If you have any issues with it make an issue on the Nuxt repo or VueUseHead and they'll look into it.

When would you choose to link CSS via useHead versus the CSS option in nuxt.config?

The Nuxt CSS option does some CSS optimization, inlining it. If you use useHead it would be a link and you'd have to make an HTTP request for the style sheet. Stick to the CSS option, unless you need some sort of async script, that is not essential for your app you can use useHead for it.

Beyond Static: Building with Nuxt 3 and Nitro

In Nuxt 3, it is possible to create fully hybrid sites. Which have individual route rules, which enable different rendering strategies for different sections of your website.

To explore that we'll create a little blog site and see what that might be like to deploy.

If you want to start a new project. It's recommended to go to nuxt.new There are a couple of templates there, but some interesting things are coming soon.

You can follow the Nuxt official installation Docs

Open a terminal and run npx nuxi init appName

npm install
npm i zod @picocss/pico

app.vue -- is the entry point to our app.

nuxt.config.ts -- No need to do anything here, for now.

nuxt.json -- has nuxt as a dev dependency and the dependencies we just installed should be automatically added

tsconfig.json -- is going to enable our editor IDE to know a little bit about the Nuxt environment, it's running in.

Start the dev server

npm run dev -- -o

Add a <script> tag to the app.vue file

and import the @picocss/pico dependency. It will add some styles to the app. That is going to make it a little bit nicer.

<!-- app.vue -->
 
<template>
  <div>
    <NuxtWelcome />
  </div>
</template>
 
<script setup lang="ts">
import "@picocss/pico"
</script>
<!-- app.vue -->
 
<template>
	<main class="container">
		<nav>
			<ul>
				<li><NuxtLink to="/">Home</NuxtLink></li>
				<li><NuxtLink to="/about">About</NuxtLink></li>
			</ul>
		</nav>
		<div>
			<NuxtPage />
		</div>
	</main>
</template>
 
<script setup lang="ts">
import "@picocss/pico"
</script>
<!-- pages/index.vue-->
 
<script setup lagn="ts">
// using the fetch API
const posts = await fetch('https://jasonplaceholder.typicode.com/posts')
	.then((response) => response.json())
</script>
<!-- pages/index.vue-->
 
<script setup lang="ts">
// Using Nuxt $fetch is the same, but abstracts away some of the boilerplate code
const posts = await $fetch('https://jasonplaceholder.typicode.com/posts')
</script>
<!-- pages/index.vue-->
 
<script setup lang="ts">
// The above is going to make the data get fetched on the server and also on the client.
// Because it's running directly in the script setup.
// Nuxt provides a composable called useFetch, which prevents that.
// It also gives some other useful helper functions and utils, it will give us an error object, to tell us if an error has happened, ...
// The key benefit is it plays well with server and client
 
const { data: posts } = await useFetch(
	'https://jasonplaceholder.typicode.com/posts'
)
</script>
<!-- pages/index.vue-->
 
<script setup lang="ts">
const { data: posts } = await useFetch(
	'https://jasonplaceholder.typicode.com/posts', {
	transform: data=> z.array(postSchema).parse(data)
})
</script>
 
<template>
</template>
 
<!-- pages/index.vue-->
 
<script setup lang="ts">
import { z } from 'zod'
 
// generate a schema to specify the type of the post you want to generate
// Zod helps with schema generation. To specify the type of the post we want to validate.
const postSchema = z.object({
  id: z.number(),
  title: z.string(),
  body: z.string(),
  userId: z.number(),
})
 
// Fetch json placeholder data
const { data: posts } = await useFetch(
  'https://jsonplaceholder.typicode.com/posts', {
    transform: data=> z.array(postSchema).parse(data)
  })
</script>
 
<!-- pages/index.vue-->
 
<script setup lang="ts">
import { z } from 'zod'
 
// generate a schema to specify the type of the post you want to generate
// Zod helps with schema generation. To specify the type of the post we want to validate.
const postSchema = z.object({
  id: z.number(),
  title: z.string(),
  body: z.string(),
  userId: z.number(),
})
 
// Fetch json placeholder data
const { data: posts } = await useFetch(
  'https://jsonplaceholder.typicode.com/posts', {
    transform: data=> z.array(postSchema).parse(data)
  })
</script>
 
<template>
 <div>
   <h1>Blog Posts</h1>
   <article v-for="post in posts" :key="post.id" >
    <header><strong>{{ post.body }}</strong></header>
    <p></p>
    <NuxtLink :to="`/posts/${post.id}`">Read more &raquo;</NuxtLink>
   </article>
 </div> 
</template>
 
<template>
  <main class="container">
    <nav>
      <ul>
        <li><NuxtLink to="/">Home</NuxtLink></li>
        <li><NuxtLink to="/about">About</NuxtLink></li>
      </ul>
    </nav>
    <div>
      <NuxtPage />
    </div>
  </main>
</template>
 <script setup lang="ts">
import "@picocss/pico"
</script>

Posts/Landing page with a list of posts and a link to each

<script setup lang="ts">
import { z } from 'zod'
 
// generate a schema to specify the type of the post you want to generate
// Zod helps with schema generation. To specify the type of the post we want to validate.
const postSchema = z.object({
  id: z.number(),
  title: z.string(),
  body: z.string(),
  userId: z.number(),
})
 
// Grab the post's id from the current route
const id = useRoute().params.id
// Fetch json placeholder data of this particular post
const { data: post } = await useFetch(
  `https://jsonplaceholder.typicode.com/posts/${id}`, 
  {
    transform: data=> postSchema.parse(data)
  }
  )
  // Handle the possibility of the post object being null
  if (!post.value) {
    throw createError({
      statusCode: 404,
      message: 'Post not found'
    })
  }
 
</script>
 
<template>
 <div>
   <h1>{{ post.title }}</h1>
    <p>
      {{ post.body }}
    </p>
    <NuxtLink to="/">Back Home</NuxtLink>
 </div> 
</template>
 

Here's the result when clicking on a post.

Post page with a title and body and a "Back Home" button

404 page with "Post not found" text

Finally to deploy we might want to have some configurable rules at nuxt.config.ts.

// nuxt.config.ts
 
export default defineNuxtConfig({
	routeRules: {
		// my about page is going to be static. I don't want to revalidate that page. It's complete. Once it's rendered, once. It can be saved in a CDN somewhere and used to render the following responses.
		'/about': { static: true },
		// But the posts page I might want to be revalidated. So we can turn it on still while we revalidate, and then every 60 seconds this will be refreshed, if necessary. 	
		'/posts/*': { swr: 60}
	}
})

We can also create a server API route server/api/test.ts

// server/api/test.ts
 
export default defineEventHandler(async event => {
	return { foo: 'bar'}
})

We might make this accessible to other apps and turn on cors mode in this case at nuxt.config.ts

// nuxt.config.ts
 
export default defineNuxtConfig({
	routeRules: {
		'/about': { static: true },
		'/posts/*': { swr: 60},
		'/api/test': { cors: true }
	}
})

Turn off the dev server $ ctrl c If you run npm build a node server will be built for you. If you deploy it to any hosting service, Vercel, Netlify, Cloudflare Pages, ... Nuxt will try and detect the environment and create the right kind of build.

If you want to test it locally you can pass the NITRO_PRESET environment variable or just configure it in nuxt.config

NITRO_PRESET=netlify npm run build

Everything is going to be put into a single folder that can be deployed. Any needed dependency will also be located in this folder, so it will not need to be installed again. Normally it's a .output folder, but in this case, it's a .netlify folder.

There's also a dist folder which will have all the things that Netlify is going to the server from CDN. This will also include _headers and _redirects files which will configure Netlify to use builder functions.

.netlify folder prepared for deploying to Netlify

Deploying the output to Netlify will give you a URL to test it on. Using the link we can visit the site and see it working as expected.

In the terminal, we can have a look at the headers for requests

http URL_from_Netlify

If we fetch the home page, every time we fetch it's a new request, a newly rendered page (Age=0)

For the about page, the first fetch request is the age=0, but on subsequent requests, it keeps going up. Because it's never going to be generated again it's a fully static page. The same is true for posts pages.

It means that you don't need to redeploy when you change the content of CMS if you're using hybrid rendering.

Tools To Explore