Nuxt Fundamentals
Workshop
Welcome!
- Full Time Instructor @VueSchool
- Masterclass Lead Instructor
- Full Stack developer
ostafa
1300+ Video Lessons
150,000 users
Alex Kyriakidis
Daniel Kelly
Debbie O'Brien
Chris Fritz
Maria Lamardo
Roman Kuba
Sébastien Chopin
Filip Rakowski
Mostafa Said
Rolf Haug
Presentation will look much like what we're doing now
Questions are taken from the chat.
Exercises are your time to practice what you've learned
(show structure in repo)
Let's join rooms and meet each other!
⏰ 5 mins
Get ready
We've got a lot to cover
1
Section
Built on the top of Vue.js 3
Build Interactive UI Elements
Interactive Forms
Cookie Consent Dialogs
Carousels
Every repetitive task is automated
You won't have to worry about:
Knowing Vue is an absolute must for working with Nuxt.
Knowing Vue is an absolute must for working with Nuxt.
It powers all the view layers and shares concepts such as reactivity, composables, and more with Nuxt.
v18.0.0
or newernpx nuxi@latest init <project-name>
Creating a new Nuxt app is as simple as running the following command:
Need to install the following packages:
nuxi@3.13.1
Ok to proceed? (y)
You will be asked to install Nuxt CLI (Nuxi)
Need to install the following packages:
nuxi@3.13.1
Ok to proceed? (y)
You will be asked to install Nuxt CLI (Nuxi)
This is needed to run nuxt
commands
❯ Which package manager would you like to use?
● npm
○ pnpm
○ yarn
○ bun
Select you preferred package manager
Open the project in VS Code
Nuxt uses an opinionated directory structure to automate repetitive tasks and allow developers to focus on pushing features.
Nuxt uses an opinionated directory structure to automate repetitive tasks and allow developers to focus on pushing features.
We will talk about the directory structure in details as we go through the material.
Nuxt uses an opinionated directory structure to automate repetitive tasks and allow developers to focus on pushing features.
We will talk about the directory structure in details as we go through the material.
Nuxt 4 will bring changes to the directory structure.
💡
Start a new dev server by running:
pnpm dev
// Or
npm run dev
This will run a script that triggers the Nuxt CLI to start a Vite development server.
// package.json
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev",
// ..
},
"dependencies": {
// ..
}
}
The browser will open
and the Nuxt welcome page will render 🎉
The browser will open
and the Nuxt welcome page will render 🎉
The App.vue component is the Vue instance's root component.
Questions?
🙋🏾♀️
👩💻👨🏽💻
Exercise 1
Did you setup the project locally?
2
Section
Before we start with Nuxt rendering modes..
It is the process of converting code into viewable, interactive web content
Both the browser and server are capable of
running the JS files needed to render Vue components
Converting .vue
SFCs
to HTML elements
Both the browser and server are capable of
running the JS files needed to render Vue components
So, a server-side rendered app is one
that renders HTML content on the server
https://vueschool.io
Request
https://vueschool.io
Server
Server
Request
https://vueschool.io
HTML
Render
Request
https://vueschool.io
The HTML files contains the actual HTML elements
of the page
<!DOCTYPE html>
<html>
<head> .. </head>
<body>
<main> Actual content .. </main>
</body>
</html>
🔎
HTML
Server
Render
Request
https://vueschool.io
HTML
Server
Render
Response
Request
HTML
Server
Render
https://vueschool.io
Request
HTML
Server
Render
https://vueschool.io
No Rendering Step
Request
https://vueschool.io
Server
Render
The HTML files only references the necessary JavaScript files to render the HTML content in the browser
🔎
HTML
<!DOCTYPE html>
<html>
<head> .. </head>
<body>
<div id='app'></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Request
https://vueschool.io
Server
HTML
Response
Render
Request
https://vueschool.io
Server
HTML
Response
Render
👑
👑
1.
1.
Universal rendering mode is enabled by default, right out of the box
1.
The server renders the .vue
files and sends a fully rendered HTML response to the browser.
1.
The server renders the .vue
files and sends a fully rendered HTML response to the browser.
The browser will receive the rendered HTML and display it to the user immediately.
1.
At this point, this is just typical server-side rendering
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Server
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Browser
Static HTML
Static HTML
1.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Server
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Browser
Static HTML
Static HTML
But, Nuxt is secretly doing its magic behind the scenes 🤫
1.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Server
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Browser
Static HTML
Static HTML
To keep client-side features like dynamic interfaces and transitions,
the browser loads JavaScript after the HTML is downloaded
1.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Server
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Browser
Static HTML
Dynamic HTML
Vue.js then takes over to enable interactivity,
achieving universal rendering
1.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Browser
Dynamic HTML
Making a static page interactive in the browser is known as 'hydration'
1.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Browser
Dynamic HTML
After the initial page visit,
all subsequent navigation is handled entirely via client-side rendering. The page doesn't need to refresh again.
1.
Your Vue app runs once on the server to render HTML
And runs again on the browser to make the page interactive during the hydration process
2.
2.
The server sends back the response without rendering the HTML elements.
The entire Vue app is rendered in the browser.
HTML elements gets generated AFTER the browser downloads and executes the JavaScript files.
2.
export default defineNuxtConfig({
ssr: false
})
You can enable the client-side mode by setting the ssr
property to false in the nuxt.config.ts
file
2.
We'll cover Nuxt configuration in more detail in a later chapter
This file is one of the files you get when you first install Nuxt
3.
3.
Previously, every route in a Nuxt app had to use the same rendering mode, either universal or client-side
3.
You can choose the rendering mode or caching rule for each page based on your needs
export default defineNuxtConfig({
routeRules: {
// Admin dashboard renders only on client-side
'/admin/**': { ssr: false },
// Homepage generated/pre-rendered at build time
'/': { prerender: true },
// Products page generated on demand, revalidates in background, cached until next change
'/products': { swr: true },
// Blog post page generated on demand once until next deployment, cached on CDN
'/blog/**': { isr: true }
}
})
4.
4.
It allows your application to be rendered closer to users through CDN edge servers, improving performance and reducing latency
4.
It allows your application to be rendered closer to users through CDN edge servers, improving performance and reducing latency
A CDN (Content Delivery Network) is a network of distributed servers that deliver web content to users based on their geographic location, improving load times and reducing latency.
💡
Questions?
🙋🏾♀️
Exercise 2
👩💻👨🏽💻
Coffee Break
☕️
⏰ 15 mins
3
Section
1.
By default, routing is disabled in a new Nuxt app.
The project only includes the App.vue
root component to render your content.
By default, routing is disabled in a new Nuxt app.
The project only includes the App.vue
root component to render your content.
Vue Router will not be included in your app's bundle.
To enable routing in Nuxt,
create a pages
directory in the project's root
Then, use the <NuxtPage />
component in App.vue
<template>
<div>
<NuxtPage />
</div>
</template>
Then, use the <NuxtPage />
component in App.vue
<template>
<div>
<NuxtPage />
</div>
</template>
The App.vue
component is optional in Nuxt.
If you don't need to customize the root component, you can remove it, and Nuxt will automatically create it for you behind the scenes.
💡
Each Vue file inside the pages/
directory generates a corresponding URL (or route) that displays the file's contents
Each Vue file inside the pages/
directory generates a corresponding URL (or route) that displays the file's contents
But those files needs to follow specific naming conventions
A file named index.vue
nested directly inside the pages
directory
will serve as the website's landing page (Homepage)
/
If the index.vue
file is nested under a directory, it will become the landing page for that route
/about
If a file has any other name, Nuxt will use that name to define the route
/about
/
You can create nested routes by placing a file inside a directory
/company/about
Multiple routes can be grouped into a directory (enclosed in parentheses) without affecting file-based routing
/about
/contact
/
v3.13 +
You can create routes with dynamic parameters
by placing the text within square brackets
/users/johndoe1
It also works if used with directory names
/active-users/1
/inactive-users/1234
If you want a parameter to be optional,
enclose it in double square brackets
/products/macbook
/products
Product: macbook
All Products
Create catch-all routes by wrapping the file name in square brackets and prefixing it with three dots
/undefined-route
404 Page Not Found
Create catch-all routes by wrapping the file name in square brackets and prefixing it with three dots
/undefined-route
404 Page Not Found
Page components should generally have a single root element.
💡
// pages/about.vue
<template>
<div>
<!-- Page content -->
</div>
</template>
To navigate between pages of your app, you should use the <NuxtLink> component
<template>
<NuxtLink to="/">Home page</NuxtLink>
</template>
The <NuxtLink>
component ensures that internal navigation within the app won't trigger a full page reload
The <NuxtLink>
component ensures that internal navigation within the app won't trigger a full page reload
It makes page navigation work like a single-page application (SPA), avoiding a full page reload from the server for each new page
The <NuxtLink>
component ensures that internal navigation within the app won't trigger a full page reload
It makes page navigation work like a single-page application (SPA), avoiding a full page reload from the server for each new page
Under the hood, the <NuxtLink>
component is a wrapper
around Vue Router's <RouterLink>
component.
It renders as a regular anchor tag in the browser, but it functions differently.
💡
<a href="/">Home Page</a>
To navigate between pages of your app, you should use the <NuxtLink> component
<template>
<NuxtLink to="/">Home page</NuxtLink>
</template>
The 'to'
prop is required and is used to define the link path
To navigate between pages of your app, you should use the <NuxtLink> component
<template>
<NuxtLink to="/">Home page</NuxtLink>
</template>
The 'to'
prop is required and is used to define the link path
You can still use the href
attribute on the NuxtLink
component—it's just an alias for the to
prop. If both to
and href
are present, href
will be ignored.
💡
The value of the prop could be a string
<template>
<NuxtLink to="/">Home page</NuxtLink>
</template>
Or, it could be an object
<template>
<NuxtLink :to="{ path: '/' }">Home page</NuxtLink>
</template>
Or, it could be an object
<template>
<NuxtLink :to="{ path: '/' }">Home page</NuxtLink>
</template>
Don't forget to bind the prop
The object syntax can be used to define parameters and query strings
Let's have a look at this example 👇
<NuxtLink
:to="{
name: 'products-category',
params: { category: 'tech' },
query: { sortBy: 'desc' },
}"
> See Our Tech Products </NuxtLink>
<NuxtLink
:to="{
name: 'products-category',
params: { category: 'tech' },
query: { sortBy: 'desc' },
}"
> See Our Tech Products </NuxtLink>
/products/tech?sortBy=desc
Let's have a look at this example 👇
<NuxtLink
:to="{
name: 'products-category',
params: { category: 'tech' },
query: { sortBy: 'desc' },
}"
> See Our Tech Products </NuxtLink>
/products/tech?sortBy=desc
Let's have a look at this example 👇
You can get the page's route name name using
Nuxt DevTools, which is enabled by default in new Nuxt apps.
💡
<script setup>
const navigateToTechProducts = () => {
navigateTo({
name: "products-category",
params: {
category: "tech",
},
query: {
sortBy: "desc",
},
});
};
</script>
<template>
<button @click="navigateToTechProducts">See Our Tech Products</button>
</template>
The navigateTo()
helper can be used for programmatic navigation
Nuxt enables us to attach Meta data to the page
We can use the definePageMeta
macro in pages components
to define meta data for the page
<script setup>
definePageMeta({
title: "My about page",
name: "custom-name",
});
</script>
<template>
<div>
<p>Meta Title: {{ $route.meta.title }}</p>
<p>Route Name: {{ $route.name }}</p>
</div>
</template>
2.
All components must be placed inside the components
directory
<script setup>
// No imports needed..
</script>
<template>
<AppHeader />
</template>
Nuxt automatically imports all components in this directory
Component names are registered based on
their directory path and filename
<template>
<BaseAppHeader />
</template>
If the component is already named according to its path,
Nuxt will not append the path to the name.
Instead, it will register the component using the existing name
<template>
<BaseAppHeader />
</template>
To dynamically import a component (also known as lazy-loading),
simply add the Lazy
prefix to the component's name.
<template>
<LazyBaseAppHeader v-if="condition" />
</template>
To dynamically import a component (also known as lazy-loading),
simply add the Lazy
prefix to the component's name.
<template>
<LazyBaseAppHeader v-if="condition" />
</template>
Use import.meta.server/client
to conditionally execute code based on the environment
💡
<script setup>
if (import.meta.server) {
// Execute only on the server
}
if (import.meta.client) { // or import.meta.browser
// Execute only on the client/browser
}
</script>
v3.12 +
And more..
3.
Different pages can have different layouts
3.
3.
Layouts are enabled by adding <NuxtLayout>
to the App.vue file
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
3.
Create the layouts
directory in the root of your Nuxt App
3.
Inside the layouts
directory, the default.vue
component will be used as the default layout for all pages.
3.
Layout components should have a single root element.
Inside the layouts
directory, the default.vue
component will be used as the default layout for all pages.
3.
// layouts/default.vue
<template>
<div>
<nav></nav>
<SidebarComponent />
<main>
<slot />
</main>
<footer></footer>
</div>
</template>
In a layout file, the content of the page
will be displayed in the <slot />
3.
We can add as many custom layouts as we want
3.
To specify the desired layout in the page component,
we can use the definePageMeta
macro
// pages/profile.vue
<script setup>
definePageMeta({
layout: 'custom'
})
</script>
3.
Layout can also be specified dynamically at runtime
by using the setPageLayout
helper from Nuxt
// pages/about.vue
<script setup>
const enableCustomLayout = () => {
setPageLayout('custom')
}
definePageMeta({
layout: false,
});
</script>
<template>
<div>
<button @click="enableCustomLayout">Update layout</button>
</div>
</template>
3.
Alternatively, the <NuxtLayout>
component accepts a name
prop
// App.vue
<script setup lang="ts">
// You might choose this based on an API call or logged-in status
const layout = "custom";
</script>
<template>
<NuxtLayout :name="layout">
<NuxtPage />
</NuxtLayout>
</template>
3.
If we want to use the <NuxtLayout>
component directly in pages instead of App.vue
, set layout
to false
and use the name
prop
// pages/about.vue
<script setup>
definePageMeta({
layout: false,
})
</script>
<template>
<div>
<NuxtLayout name="custom">
The rest of the page
</NuxtLayout>
</div>
</template>
Questions?
🙋🏾♀️
Exercise 3
👩💻👨🏽💻
4
Section
script setup
will be triggered twice<script setup>
const data = ref()
try {
const result = await fetch("api/endpoint");
data.value = result
} catch (error) {
console.log(error);
}
</script>
Let's take a look at this code
The fetch request will be executed on the server
Let's take a look at this code
<script setup>
const data = ref()
try {
const result = await fetch("api/endpoint");
data.value = result
} catch (error) {
console.log(error);
}
</script>
Then, it will be executed again in the browser
The fetch request will be executed on the server
<script setup>
const data = ref()
try {
const result = await fetch("api/endpoint");
data.value = result
} catch (error) {
console.log(error);
}
</script>
Let's take a look at this code
To solve this..
useFetch()
$fetch()
useAsyncData()
1.
1.
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
useFetch
is the most straightforward way for fetching data
1.
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
We must always use await
with useFetch()
and useAsyncData()
composables
1.
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
It takes a string of the api endpoint as the first argument
1.
useFetch
should only be used directly in a setup function, plugin, or route middleware.
💡
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
It takes a string of the api endpoint as the first argument
1.
The useFetch composable supports network requests de-duplication
1.
The useFetch composable supports network requests de-duplication
Requests made on the server won't trigger again on the client-side
1.
The fetch request will be executed on the server
<script setup>
const { data } = await useFetch('/api/dadjokes')
</script>
1.
Since the API call is made on the server, the data is forwarded to the client in the payload
The fetch request will be executed on the server
<script setup>
const { data } = await useFetch('/api/dadjokes')
</script>
1.
<script setup>
const { data } = await useFetch("/api/dadjokes");
</script>
<template>
<p>{{ data?.joke }}</p>
</template>
1.
<script setup>
const { data } = await useFetch("/api/dadjokes");
</script>
<template>
<p>{{ data?.joke }}</p>
</template>
Text
Auto-generated key for the fetched data
Text
1.
<script setup>
const { data } = await useFetch("/api/dadjokes");
</script>
<template>
<p>{{ data?.joke }}</p>
</template>
Text
Auto-generated key for the fetched data
Text
The fetched data forwarded to the client-side
1.
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
The composable will always return an object with many properties:
{ data, status, error, refresh, execute, clear }
and others
1.
{
data,
status,
error,
refresh,
execute,
clear
}
All three are Refs and should be accessed with .value
1.
{
data,
status,
error,
refresh,
execute,
clear
}
All three are Refs and should be accessed with .value
All three are pure functions
1.
We can pass some helpful options to useFetch
<script setup>
const { data, error } = await useFetch('/api/endpoint', {
// options..
})
</script>
1.
<script setup>
const { data, error } = await useFetch('/api/endpoint', {
pick: ['title'],
})
</script>
If the handler function returns a single object, the pick
option allows selecting specific keys from it
1.
<script setup>
const { data, error } = await useFetch('/api/endpoint', {
server: false,
})
</script>
Change to false if you want to prevent firing the fetch request
on the server (Defaults to true
)
1.
<script setup>
const { data, error } = await useFetch('/api/endpoint', {
transform: (result) => {
const changedResult = // Do something with the result
return changedResult
}
})
</script>
The transform
option is a function used to modify the handler function's result.
1.
There are many other great options to explore:
2.
2.
<script setup>
const result = await $fetch('/api/endpoint')
</script>
The $fetch()
function is to perform a simple fetch request
2.
<script setup>
const result = await $fetch('/api/endpoint')
</script>
The $fetch()
function is to perform a simple fetch request
The $fetch
function will not provide network calls de-duplication
💡
2.
<script setup>
const submitComment = async () => {
const comment = await $fetch('/api/comments', {
method: 'POST',
body: {
// Comment data
}
})
}
</script>
It is recommended to use $fetch
for client-side interactions (event based)
or combined with useAsyncData()
2.
<script setup>
const submitComment = async () => {
const comment = await $fetch('/api/comments', {
params: {
id: 123
}
})
}
</script>
We can pass an object as the second argument with the preferred options
The $fetch()
utility is using the ofetch
package under the hood.
Explore the ofetch
repo for more usage info:
2.
3.
<script setup>
const { data, error } = await useAsyncData(() => $fetch('/api/endpoint'))
</script>
useAsyncData()
is another way to fetch data in Nuxt
3.
3.
<script setup>
const { data, error } = await useAsyncData('users-comments', () => getComments())
</script>
It accepts a string as the first argument to define a unique key for the fetched data in the payload (recommended)
It accepts a string as the first argument to define a unique key for the fetched data in the payload (recommended)
3.
<script setup>
const { data, error } = await useAsyncData('users-comments', () => getComments())
</script>
The key is used to cache the response of the second argument
3.
<script setup>
const { data, error } = await useAsyncData(() => getComments())
</script>
If no key provided, Nuxt will auto generate a key
3.
Let's see what caching actually looks like
3.
<script setup>
const { data, error } = await useAsyncData(() => $fetch('/api/endpoint'))
</script>
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
Nearly equivalent to
<script setup>
const { data, error } = await useAsyncData(() => $fetch('/api/endpoint'))
</script>
3.
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
Nearly equivalent to
In fact, useFetch
is using useAsyncData
under the hood
<script setup>
const { data, error } = await useAsyncData(() => $fetch('/api/endpoint'))
</script>
3.
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
Nearly equivalent to
But It's designed for different use cases
In fact, useFetch
is using useAsyncData
under the hood
<script setup>
const { data } = await useAsyncData('users-comments', async () => {
const usersIds = await getUsersIds()
const usersComments = await getComments(usersIds)
return usersComments
})
</script>
3.
With useAsyncData()
, we can make multiple fetch requests,
and it will return the data only once all promises are resolved
<script setup>
const { data } = await useAsyncData('users-comments', async () => {
const usersIds = await getUsersIds()
const usersComments = await getComments(usersIds)
return usersComments
})
</script>
3.
We can add async
to the callback to control the order of request execution with await
, especially if one request depends on the result of another
<script setup>
const { data: discounts, status } = await useAsyncData('cart-discount', async () => {
const [coupons, offers] = await Promise.all([
$fetch('/cart/coupons'),
$fetch('/cart/offers')
])
return { coupons, offers }
})
</script>
3.
Also, we can use async
to await
Promise.all
with multiple unrelated requests, ensuring they execute in parallel.
<script setup>
const getComments = async () => {
const { data, error } = await supabase.from('comments').select()
if(error) return // handle error
return data
}
const { data } = await useAsyncData('users-comments', () => getComments())
</script>
3.
useAsyncData()
is also useful when dealing with a third-party query layer
For example, working with Supabase or Firebase SDKs
<script setup>
const getComments = async () => {
const { data, error } = await supabase.from('comments').select()
if(error) return // handle error
return data
}
const { data } = await useAsyncData('users-comments', () => getComments())
</script>
3.
In this example, we don't return the data directly; instead, we handle the incoming data from the SDK before returning it
useAsyncData()
returns almost the same properties
we get from useFetch()
3.
<script setup>
const { data, error } = await useAsyncData(() => getComments())
</script>
<script setup>
const { data, error } = await useFetch('/api/endpoint')
</script>
If a simple fetch request is needed,
it's more straightforward to just use useFetch()
3.
<script setup>
const { data, error } = await useAsyncData(() => $fetch('/api/endpoint'))
</script>
By default, useFetch()
and useAsyncData()
will block the navigation until the data is fetched
To prevent this behavior, we can set the lazy
option to true
<script setup>
const { data, error } = await useFetch('/api/endpoint', {
lazy: true
})
</script>
<script setup>
const { data, error } = await useAsyncData(() => {
$fetch("/api/endpoint");
},
{ lazy: true }
);
</script>
Or, use useLazyFetch
and useLazyAsyncData
composables
<script setup>
const { data, error } = await useLazyFetch('/api/endpoint')
</script>
<script setup>
const { data, error } = await useLazyAsyncData(() => {
$fetch("/api/endpoint");
}
);
</script>
Questions?
🙋🏾♀️
Exercise 4
👩💻👨🏽💻
Coffee/Launch Break
☕️
⏰ 25 mins
5
Section
By default, Nuxt displays an error page when it encounters a fatal error
It will also render a default error page for undefined routes
We can fully customize the error page by creating an error.vue
component in the project's root directory
The Nuxt error page is not a route.
Therefore, it can't go inside the pages directory.
Since it's not really a route/page,
We can't use definePageMeta
in the error.vue
component
<script setup>
definePageMeta({})
</script>
<template>
<div>
</div>
</template>
But We can use the NuxtLayout
component
and specify the name if needed
<script setup>
const props = defineProps(['error'])
</script>
<template>
<NuxtLayout name="error-layout">
<div>
<!-- Error Data -->
</div>
</NuxtLayout>
</template>
The error.vue
component accepts the error as a prop,
which can be used to access the error details
<script setup>
const props = defineProps(['error'])
</script>
<template>
<div>
<h2>{{ error.statusCode }}</h2>
<p>{{ error.statusMessage }}</p>
<p>{{ error.cause }}</p>
<p>{{ error.stack }}</p>
</div>
</template>
Nuxt offers the NuxtError
type to provide helpful type hints
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps({
error: Object as () => NuxtError
})
</script>
<template>
<div>
<h2>{{ error.statusCode }}</h2>
<p>{{ error.statusMessage }}</p>
<p>{{ error.cause }}</p>
<p>{{ error.stack }}</p>
</div>
</template>
To manually throw an error and render the error page,
we can use the createError
function
<script setup>
const slug = 'the-lord-of-the-rings'
const { data, error } = await useFetch(`/api/books/${slug}`)
if (error.value) {
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
}
</script>
In case it's used in a response to user interactions,
the createError
will not render the error page
<script setup>
const books = ref([]);
const fetchBook = async () => {
const result = await $fetch("/api/books/the-lord-of-the-rings");
if (!result) {
throw createError({ statusCode: 404, statusMessage: "Book Not Found" });
}
books.value.push(result);
};
</script>
<template>
<button @click="fetchBook">Get Favorite Book</button>
</template>
If we want to get the error page to render,
we must set the fatal
option to true
in the createError
function
<script setup>
const books = ref([]);
const fetchBook = async () => {
const result = await $fetch("/api/books/the-lord-of-the-rings");
if (!result) {
throw createError({
statusCode: 404,
statusMessage: "Book Not Found",
fatal: true,
});
}
books.value.push(result);
};
</script>
Another way to render the error page,
is by using the showError
function
<script setup>
const books = ref([]);
const fetchBook = async () => {
const result = await $fetch("/api/books/the-lord-of-the-rings");
if (!result) {
showError({
statusCode: 404,
statusMessage: "Book Not Found"
})
}
books.value.push(result);
};
</script>
Another way to render the error page,
is by using the showError
function
<script setup>
const books = ref([]);
const fetchBook = async () => {
const result = await $fetch("/api/books/the-lord-of-the-rings");
if (!result) {
showError({
statusCode: 404,
statusMessage: "Book Not Found"
})
}
books.value.push(result);
};
</script>
According to the docs, It is recommended to use
throw createError()
instead of showError()
💡
We can clear the error using the clearError
function
// error.vue
<script setup>
const props = defineProps(['error'])
const clearAndRedirect = () => {
clearError({ redirect: '/' })
}
</script>
<template>
<div>
<h2>{{ error.statusCode }}</h2>
<button @click="clearAndRedirect">Go back to the Homepage</button>
</div>
</template>
It's optional to pass a string to the function for redirecting to a different path after clearing the error
// error.vue
<script setup>
const props = defineProps(['error'])
const clearAndRedirect = () => {
clearError({ redirect: '/' })
}
</script>
<template>
<div>
<h2>{{ error.statusCode }}</h2>
<button @click="clearAndRedirect">Go back to the Homepage</button>
</div>
</template>
In Vue.js, errors propagate in a way similar to events in the DOM
They bubble up from the component that caused the error to its parent, and continue up the chain hitting the root component (App.vue
)
<script setup>
onErrorCaptured((error) => {
// Do what you want with the error
})
</script>
The onErrorCaptured
Vue lifecycle hook can be used to catch any error thrown in a descendant component
<script setup>
onErrorCaptured((error) => {
// Do what you want with the error
})
</script>
<template>
<ChildComponent />
</template>
If any error is thrown in the child component or its decedents
It will be caught by the onErrorCaptured
hook
<script setup>
onErrorCaptured((error) => {
// Do what you want with the error
})
</script>
<template>
<ChildComponent />
</template>
If any error is thrown in the child component or its decedents
<script setup>
onErrorCaptured((error) => {
// Do what you want with the error
return false;
})
</script>
<template>
<ChildComponent />
</template>
To stop the error from propagating any further, we must return false
<script setup>
const nuxtApp = useNuxtApp()
nuxtApp.hook("vue:error", (error) => {
console.log("We got an error: ", error);
});
</script>
<template>
<ChildComponent />
</template>
Nuxt also provides the vue:error
hook which is triggered when an error propagate all the way up to the root component
<script setup>
const nuxtApp = useNuxtApp()
nuxtApp.hook("vue:error", (error) => {
console.log("We got an error: ", error);
});
</script>
<template>
<ChildComponent />
</template>
Nuxt also provides the vue:error
hook which achieves the same thing
The useNuxtapp()
composable allow us to get the running Nuxt instance
The errors don't stop at the root component
They go all the way up to the global errorHandler
in Vue config
<script setup>
const nuxtApp = useNuxtApp()
nuxtApp.vueApp.config.errorHandler = (error) => {
// handle error, e.g. report to a service
}
</script>
<template>
<ChildComponent />
</template>
Questions?
🙋🏾♀️
Exercise 5
👩💻👨🏽💻
6
Section
export default defineNuxtConfig({
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
}
}
})
By adding the app.head
property in your nuxt.config.ts
,
you can customize the head section for your entire app
export default defineNuxtConfig({
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
}
}
})
By adding the app.head
property in your nuxt.config.ts
,
you can customize the head section for your entire app
We can't pass reactive data to the app.head
property.
If you need dynamic data, use the useHead()
composable instead
💡
<script setup>
const theme = ref("dark");
useHead({
title: "Nuxt Cert Bootcamp",
meta: [{ name: "description", content: "Boost your Nuxt skills.." }],
htmlAttrs: {
class: theme,
},
script: [{ innerHTML: "console.log('Hello world')" }],
});
</script>
With the useHead()
composable, we can add dynamic data to our tags
from within any component
<script setup>
const theme = ref("dark");
useHead({
title: "Nuxt Cert Bootcamp",
meta: [{ name: "description", content: "Boost your Nuxt skills.." }],
htmlAttrs: {
class: theme,
},
script: [{ innerHTML: "console.log('Hello world')" }],
});
</script>
Just like all composables,
It has to be used directly inside the script setup
<script setup>
useHead({
script: [
{
src: 'https://third-party-script.com',
// valid options are: 'head' | 'bodyClose' | 'bodyOpen'
tagPosition: 'bodyClose'
}
]
})
</script>
We can also set the tag's position by using the tagPosition property within the script object
The useSeoMeta()
composable allows you to define your page's
meta tags as a simple object, with full TypeScript support
<script setup>
useSeoMeta({
title: 'Nuxt Cert Bootcamp',
ogTitle: 'Nuxt Cert Bootcamp',
description: 'Boost your Nuxt skills..',
ogDescription: 'Boost your Nuxt skills..',
ogImage: 'https://example.com/image.png',
twitterCard: 'summary_large_image',
})
</script>
useSeoMeta()
composable helps you avoid typos and common mistakes, such as using name
instead of property
in some cases
<script setup>
useSeoMeta({
ogTitle: 'Nuxt Cert Bootcamp',
})
</script>
<script setup>
useHead({
meta: [
{ name: 'og:title', content: 'Nuxt Cert Bootcamp' }
]
})
</script>
<meta property=”og:title” content=”Nuxt Cert Bootcamp” />
<meta name=”og:title” content=”Nuxt Cert Bootcamp” />
useSeoMeta()
composable helps you avoid typos and common mistakes, such as using name
instead of property
in some cases
<script setup>
useSeoMeta({
ogTitle: 'Nuxt Cert Bootcamp',
})
</script>
<script setup>
useHead({
meta: [
{ name: 'og:title', content: 'Nuxt Cert Bootcamp' }
]
})
</script>
<meta property=”og:title” content=”Nuxt Cert Bootcamp” />
<meta name=”og:title” content=”Nuxt Cert Bootcamp” />
Check the 'Open Graph' tab in Nuxt DevTools to spot any missing important tags
💡
useSeoMeta()
composable helps you avoid typos and common mistakes, such as using name
instead of property
in some cases
<script setup>
useSeoMeta({
ogTitle: 'Nuxt Cert Bootcamp',
})
</script>
<script setup>
useHead({
meta: [
{ name: 'og:title', content: 'Nuxt Cert Bootcamp' }
]
})
</script>
<meta property=”og:title” content=”Nuxt Cert Bootcamp” />
<meta name=”og:title” content=”Nuxt Cert Bootcamp” />
It even provides ready-to-paste code snippet to fix all your missing tags!
💡
Nuxt offers <Title>
, <Base>
, <NoScript>
, <Style>
, <Meta>
, <Link>
, <Body>
, <Html>
, and <Head>
components to directly manage metadata within your component's template.
<script setup lang="ts">
const title = ref('Hello World')
</script>
<template>
<div>
<Head>
<Title>{{ title }}</Title>
<Meta name="description" :content="title" />
<Style type="text/css" children="body { background-color: green; }" ></Style>
</Head>
<!-- page content -->
</div>
</template>
Questions?
🙋🏾♀️
Exercise 6
👩💻👨🏽💻
7
Section
nuxt.config
1.
So far, we used the nuxt.config
file to:
The nuxt.config
file is the key to configuring our Nuxt app
It exists in the root directory for any Nuxt project
You can rename it to nuxt.config.js
to use it without TypeScript support
.js
But, it's recommended to use TypeScript for this file to get help from your IDE
👑
export default defineNuxtConfig({
ssr: false,
// Other configuration options..
})
The file exports the defineNuxtConfig
function,
which takes an object with configuration options
There are SOOO MANY configuration options
you don't have to know them all by heart
Nuxt offers a runtime config API to manage configuration
and secrets in your application
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: '1ae3gffdr3',
public: {
apiBase: '/api'
}
}
})
We use the runtimeConfig
property to define and expose both
secret and public data
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: '1ae3gffdr3',
public: {
apiBase: '/api'
}
}
})
Top level properties
Only available on the server
Used for secret keys
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: '1ae3gffdr3',
public: {
apiBase: '/api'
}
}
})
Top level properties
Only available on the server
Used for secret keys
Nested in public
Exposed to both client and server
Used for public data
<script setup>
const runtimeConfig = useRuntimeConfig()
console.log('Secret key: ', runtimeConfig.superSecretKey)
console.log('Public key: ', runtimeConfig.public.apiBase)
</script>
We can access runtimeConfig
anywhere in our Nuxt app using the useRuntimeConfig()
built-in composable
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: '1ae3gffdr3',
public: {
apiBase: '/api'
}
}
})
<template>
<p>API Base URL: {{ $config.public.apiBase }}</p>
</template>
Alternatively, we can use $config
to access the public runtimeConfig
properties directly in the template
export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBase: '/api'
}
}
})
What do you think will be logged to the console in a
universal rendered app? 🤔
<script setup>
const runtimeConfig = useRuntimeConfig()
console.log('Secret key: ', runtimeConfig.superSecretKey)
console.log('Public key: ', runtimeConfig.public.publicKey)
</script>
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: 'secured-key',
public: {
publicKey: 'exposed-key'
}
}
})
<script setup>
const runtimeConfig = useRuntimeConfig()
console.log('Secret key: ', runtimeConfig.superSecretKey)
console.log('Public key: ', runtimeConfig.public.publicKey)
</script>
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: 'secured-key',
public: {
publicKey: 'exposed-key'
}
}
})
SSR
Secret key: 'secured-key'
Public key: 'exposed-key'
CSR
Secret key: undefined
Public key: 'exposed-key'
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: 'secured-key',
public: {
publicKey: 'exposed-key'
}
}
})
We can check the runtime configs using the 'Runtime Configs' tab
in Nuxt DevTools
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: import.meta.env.SUPER_SECRET_KEY,
public: {
publicKey: import.meta.env.NUXT_PUBLIC_PUBLIC_KEY
}
}
})
We can assign default values to runtimeConfig
properties using environment variables from the .env
file
// .env
SUPER_SECRET_KEY=secured_key
NUXT_PUBLIC_PUBLIC_KEY=exposed_key
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: import.meta.env.SUPER_SECRET_KEY,
public: {
publicKey: import.meta.env.NUXT_PUBLIC_PUBLIC_KEY
}
}
})
// .env
SUPER_SECRET_KEY=secured_key
NUXT_PUBLIC_PUBLIC_KEY=exposed_key
But there are some rules
⚠️
// .env
NUXT_PUBLIC_PUBLIC_KEY=exposed_key
In development, it's ok to use the .env
variables in runtimeConfig
npm run dev
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
public: {
publicKey: import.meta.env.NUXT_PUBLIC_PUBLIC_KEY
}
}
})
// .env
NUXT_PUBLIC_PUBLIC_KEY=exposed_key
Once we make any changes to the .env
file, the dev server will pick it up and update the runtimeConfig
npm run dev
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
public: {
publicKey: import.meta.env.NUXT_PUBLIC_PUBLIC_KEY
}
}
})
// .env
NUXT_PUBLIC_PUBLIC_KEY=exposed_key
During the build process, the app can access the .env
file to update runtimeConfig
with static values
npm run build
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
public: {
publicKey: "exposed-key"
}
}
})
// .env
NUXT_PUBLIC_PUBLIC_KEY=exposed_key
Once the app is built, changing the values inside the .env file will not affect the app anymore
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
public: {
publicKey: "exposed-key"
}
}
})
Every time we run the production server,
It is our responsibility to provide the environment variables to the server in order to override the runtimeConfig
values
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
public: {
publicKey: "updated-key"
}
}
})
NUXT_PUBLIC_PUBLIC_KEY=updated-key node .output/server/index.mjs
Make sure to set environment variables explicitly using the tools and methods provided by your hosting platform
Vercel UI for updating environment variables
Netlify UI for Nuxt deployment
But ...
There are some more rules
⚠️
We have specific naming conventions to follow
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: import.meta.env.SUPER_SECRET_KEY,
public: {
publicKey: import.meta.env.NUXT_PUBLIC_PUBLIC_KEY
}
}
})
Prefix environment variable names with 'NUXT_
' to ensure they update at runtime
// .env
SUPER_SECRET_KEY=secured_key
NUXT_PUBLIC_PUBLIC_KEY=exposed_key
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
public: {
publicKey: import.meta.env.NUXT_PUBLIC_PUBLIC_KEY
}
}
})
Environment variable names should adhere to the structure of your runtimeConfig
keys,using underscores '_
' to separate keys and case changes
// .env
NUXT_PUBLIC_PUBLIC_KEY=exposed_key
Never render private variables or use them on the client side in any way
// nuxt.config.js
export default defineNuxtConfig({
runtimeConfig: {
superSecretKey: import.meta.env.SUPER_SECRET_KEY,
}
})
// .env
SUPER_SECRET_KEY=secured_key
<!-- app.vue -->
<template>
<div> {{ $config.superSecretKey }} </div>
</template>
Another cool thing about the nuxt.config
file
export default defineNuxtConfig({
$production: {
runtimeConfig: {
apiKey: 'production-api-key'
}
},
$development: {
runtimeConfig: {
apiKey: 'testing-api-key'
}
}
})
It's environment-aware!
app.config
2.
The app.config
file provides another way to define and expose configuration options to the app
export default defineAppConfig({
siteName: 'Nuxt Bootcamp'
// Other options..
})
The file exports the defineAppConfig
function,
which takes an object with configuration options
export default defineAppConfig({
siteName: 'Nuxt Bootcamp'
// Other options..
})
Use the useAppConfig
built-in composable to access the define configs
<script setup>
const appConfig = useAppConfig()
</script>
<template>
<h1> {{ appConfig.siteName }} </h1>
</template>
export default defineAppConfig({
siteName: 'Nuxt Bootcamp'
})
All defined configs are completely reactive and
can be changed at runtime
<script setup>
const appConfig = useAppConfig()
appConfig.siteName = "The Awesome Nuxt Bootcamp"
</script>
<template>
<h1> {{ appConfig.siteName }} </h1>
</template>
export default defineAppConfig({
siteName: 'Nuxt Bootcamp'
})
Use the updateAppConfig
function to merge new properties to the app config at runtime
<script setup>
const appConfig = useAppConfig()
appConfig.siteName = "The Awesome Nuxt Bootcamp"
updateAppConfig({siteDescription: 'Nuxt Bootcamp Codebase'})
</script>
<template>
<h1> {{ appConfig.siteName }} </h1>
<p v-if="appConfig.siteDescription">
{{ appConfig.siteDescription }}
</p>
</template>
export default defineAppConfig({
siteName: 'Nuxt Bootcamp'
})
Use the updateAppConfig
function to merge new properties to the app config at runtime
<script setup>
const appConfig = useAppConfig()
appConfig.siteName = "The Awesome Nuxt Bootcamp"
updateAppConfig({siteDescription: 'Nuxt Bootcamp Codebase'})
</script>
<template>
<h1> {{ appConfig.siteName }} </h1>
<p v-if="appConfig.siteDescription">
{{ appConfig.siteDescription }}
</p>
</template>
Don't add any sensitive data to the app.config file.
All the data defined will be exposed to the client.
💡
So...
We have to ways to expose configuration options to the app
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
secretKey: '123',
public: {
publicKey: '/api'
}
}
})
// app.config.ts
export default defineAppConfig({
foo: 'bar'
})
When Should We Use What?
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
secretKey: '123',
public: {
publicKey: '/api'
}
}
})
// app.config.ts
export default defineAppConfig({
foo: 'bar'
})
nuxt.config runtimeConfig
app.config
Questions?
🙋🏾♀️
No Exercise for this section
8
Section
Nuxt provide two primary directories to manage assets
public/
1.
assets/
2.
Nuxt provide two primary directories to manage assets
public/
1.
The public/
directory stores all static assets
The public/
directory serves static assets
at your app's root URL
<template>
<img src="/img/logo.png" alt="pizza.com logo" />
</template>
pizza.com/img/logo.png
Files in the public/
directory are not modified by the build process
Making it ideal for files that rarely change and
must keep their original names
The public/
directory can also be used to store local fonts
This allows us to register the font in our CSS and link it using url()
@font-face {
font-family: 'MySpecialFont';
src: url('/fonts/MySpecialFont.woff') format('woff');
// ...
}
<style>
h1 {
font-family: 'FarAwayGalaxy', sans-serif;
}
</style>
assets/
2.
The assets/
directory typically holds stylesheets, fonts, and images that aren’t served from the public/
directory
Files inside the assets/
directory are processed by Vite during the build process for performance or caching purposes
We can use the assets stored in the assets/
directory
by prefixing its path with ~/assets/
<template>
<img src="~/assets/img/logo.png" alt="pizza.com logo" />
</template>
Nuxt won't serve files in the assets/
directory at a static URL
<template>
<img src="~/assets/img/logo.png" alt="pizza.com logo" />
</template>
???
Stylesheets can be imported locally in components
<script setup>
// Use a static import for server-side compatibility
import '~/assets/css/base.css'
// Caution: Dynamic imports are not server-side compatible
import('~/assets/css/base.css')
</script>
<style>
@import url("~/assets/css/override.css");
</style>
Use the scoped attribute on the style tag to ensure style apply only to this component's elements
<style scoped>
@import url("~/assets/css/override.css");
</style>
For global styles, use the css
property in nuxt.config
export default defineNuxtConfig({
css: ['~/assets/css/main.css']
})
For global styles, use the css
property in nuxt.config
export default defineNuxtConfig({
css: ['~/assets/css/main.css']
})
Styles added to the css
property will be included in all pages
As for external stylesheets
export default defineNuxtConfig({
app: {
head: {
link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/....min.css' }]
}
}
})
OR
<script setup>
useHead({
link: [{ rel: 'stylesheet', href: 'https://cdnjs.cloudflare.com/....min.css' }]
})
</script>
Questions?
🙋🏾♀️
No Exercise for this section
8
Section
Nuxt apps deployment types
A Nuxt application can be deployed
Node.js server
pre-rendered for static hosting
Serverless or edge (CDN) environments
npm run build
Run the build command to build your app's production bundle
node .output/server/index.mjs
Use the .output/server/index.mjs
to start the production server you built with node
bun run .output/server/index.mjs
Other JavaScript runtimes are supported too
When and where to run those commands?
It depends on how and where do you host the app
Each hosting platform has its own deployment guide for applications
Netlify UI for Nuxt deployment
There are plenty of hosting providers for Nuxt
npx serve .output/public
To deploy static Nuxt apps,
Just generate the pages and deploy the .output/public
directory to any static hosting provider
Questions?
🙋🏾♀️
No Exercise for this section
🎉
That's all for this workshop
🙋🏾♀️
Any final questions?