4 min
September 9, 2024
Easy Ways to Handle 404 pages from Storyblok with Nuxtjs
When building a website, ensuring a seamless user experience is crucial. However, there are times when a user might navigate to a page that doesn't exist, resulting in a "404 Not Found" error. While this is a common issue, the way you handle 404 error pages can significantly impact the user experience. In this blog post, we’ll explore how to handle 404 error pages in a Nuxt.js project integrated with Storyblok as the headless CMS.
Listen to the audio version of this article.
Why Customize 404 Error Pages?
A generic 404 error page can be a dead-end for users, leading to frustration and potential loss of engagement. By customizing this page, you can:
Maintain brand consistency.
Provide useful information and navigation options.
Engage users and guide them back to relevant content.
The problem
Storyblok doesn't have clear guidelines in documentation on how to handle 404 pages. Moreover, there is no dedicated endpoint for it in Content Delivery API V2. The only way to know if story exists is to try to fetch it.
Option A (handle errors directly on the page)
Trigger context.error everywhere we fetch data for pages if we catch an error. It will be looking something like this
export default defineComponent({
setup(_props, context: any) {
const storyblokApi = useStoryblokApi();
const { fetch } = useFetch(async () => {
try {
const { data } = await storyblokApi.get(`cdn/stories/test-story`, {
version: 'draft',
});
const results = (data as any)?.story ?? null;
// save story
} catch (error) {
console.log('error', error);
if (typeof error === 'string') {
const parsedError = JSON.parse(error as string);
context.error({ statusCode: parsedError.status ?? 500 });
} else {
context.error({ statusCode: 500 });
}
}
});
}
})
The problem with this solution as we have to copy paste this code in many places in our codebase. Another reason not to use this option is because we see the page layout for a moment before being redirected to 404 page. To resolve this issue we can use middleware.
Option B (middleware + pinia)
Create middleware to handle storyblok fetching and save the result to global store. We will be using pinia store.
stores/storyblok.ts
import { defineStore } from 'pinia';
interface StoryblokState {
currentStory: any;
}
export const useStoryblokStore = defineStore('storyblok', {
state: (): StoryblokState => ({
currentStory: {},
}),
});
middleware/story-url-resolver.ts
import { Middleware } from '@nuxt/types';
import { useStoryblokStore } from '~/stores/storyblok';
import { storeToRefs } from 'pinia';
const urlResolverMiddleware : Middleware = async (context) => {
const storyblokApi = useStoryblokApi();
const storyblokStore = useStoryblokStore();
const { path } = context.route;
try {
const { data } = await storyblokApi.get(`cdn/stories/${path}`, {
version: 'draft',
});
const results = (data as any)?.story ?? null;
storyblokStore.$patch((state) => {
state.currentStory = results;
});
} catch (error) {
console.log('error', error);
if (typeof error === 'string') {
const parsedError = JSON.parse(error as string);
context.error({ statusCode: parsedError.status ?? 500 });
} else {
context.error({ statusCode: 500 });
}
}
};
This solution seems to be working, but now we face a known bug in Nuxtjs v2 mentioned here.
In general, the issue was that at the time of parallel opening of the page (on any device), the application context was shared and page B had in the SSR data of page A which resulted in an error in rendering the content of the page.
Option C (middleware + context)
Now we're not gonna be using pinia store, we're going to save story directly into middleware's context instead.
import { Middleware } from '@nuxt/types';
const urlResolverMiddleware : Middleware = async (context) => {
const storyblokApi = useStoryblokApi();
const { path } = context.route;
try {
const { data } = await storyblokApi.get(`cdn/stories/${path}`, {
version: 'draft',
});
const results = (data as any)?.story ?? null;
// crucial change
context.app.currentStory = results;
} catch (error) {
console.log('error', error);
if (typeof error === 'string') {
const parsedError = JSON.parse(error as string);
context.error({ statusCode: parsedError.status ?? 500 });
} else {
context.error({ statusCode: 500 });
}
}
};
We need to make small adjustments in our pages components to use asyncData in order to get data from context on client and on the server
<script lang="ts">
export default {
layout: 'basic',
async asyncData(context) {
return { currentStory: context.app.currentStory }
},
middleware: [
'story-url-resolver',
],
};
</script>