Personal blog finxol.io
blog

fix: improve error page

finxol.io 9105bf6f 70d67434

verified
+185 -144
+1 -1
.zed/settings.json
··· 1 1 { 2 - "language_servers": ["deno", "..."], 2 + "language_servers": ["deno", "biome", "..."], 3 3 "languages": { 4 4 "Markdown": { 5 5 "language_servers": ["harper-ls", "..."]
+3 -128
app/app.vue
··· 1 1 <script setup> 2 - import { useDark, useToggle } from "@vueuse/core"; 3 - 4 - const config = useRuntimeConfig().public; 5 - 6 - const pageBackground = ref("bg-stone-100 dark:bg-neutral-900"); 7 - 8 - const isDark = useDark(); 9 - const toggleDark = useToggle(isDark); 10 - 11 - useHead({ 12 - title: config.title, 13 - meta: [ 14 - { 15 - name: "viewport", 16 - content: "width=device-width, initial-scale=1" 17 - }, 18 - ...config.meta 19 - ], 20 - bodyAttrs: { 21 - class: pageBackground 22 - } 23 - }); 24 - 25 - const { data } = await useAsyncData("navigation", () => { 26 - return queryCollectionNavigation("pages", ["path"]); 27 - }); 28 - 29 - const pages = data.value ? data.value[0]?.children : []; 30 - 31 - const links = ref( 32 - [ 33 - { 34 - icon: "ant-design:github-filled", 35 - title: "GitHub", 36 - href: config.links.github 37 - }, 38 - { 39 - icon: "ri:mastodon-fill", 40 - title: "Mastodon", 41 - href: config.links.mastodon 42 - }, 43 - { 44 - icon: "ri:bluesky-fill", 45 - title: "BlueSky", 46 - href: config.links.bluesky 47 - } 48 - ].reverse() 49 - ); 50 - 51 - const date = new Date(); 52 - 53 - function scrollToTop() { 54 - window.scrollTo({ 55 - top: 0, 56 - behavior: "smooth" 57 - }); 58 - } 59 2 </script> 60 3 61 4 <template> 62 - <div :class="[ 63 - 'page-container', 64 - pageBackground, 65 - 'text-gray-800 dark:text-gray-300', 66 - 'min-h-screen max-w-4xl', 67 - 'grid grid-cols-1 grid-rows-[auto_1fr_auto]', 68 - 'mx-auto px-6', 69 - ]"> 70 - <header :class="[ 71 - 'border-b-2 border-stone-200 dark:border-stone-800', 72 - 'py-6 md:py-8', 73 - 'flex justify-between align-center', 74 - ]"> 75 - <div class="flex items-center gap-4 sm:gap-6"> 76 - <img src="/logo.png" alt="Logo" class="hidden sm:block h-8" /> 77 - <NuxtLink to="/" class="text-xl leading-5 sm:text-2xl font-medium font-serif-bold"> 78 - {{ config.title }} 79 - </NuxtLink> 80 - </div> 81 - 82 - <div class="flex items-center gap-4 sm:gap-8"> 83 - <Country /> 84 - <div 85 - class="cursor-pointer" 86 - @click="toggleDark()" 87 - > 88 - <Icon v-if="isDark" name="ri:sun-fill" size="1.5rem" mode="svg" /> 89 - <Icon v-else name="ri:moon-fill" size="1.5rem" mode="svg" /> 90 - </div> 91 - <nav :class="['flex items-start gap-4', 'text-gray-800 dark:text-gray-200', 'font-semibold']"> 92 - <template v-for="item in pages" :key="item.path"> 93 - <NuxtLink v-if="item.title" :to="item.path.replace('/pages', '')"> 94 - {{ item.title }} 95 - </NuxtLink> 96 - </template> 97 - </nav> 98 - 99 - <div class="flex items-center gap-2"> 100 - <template v-for="link in links" :key="link.title"> 101 - <NuxtLink v-if="link.href" :to="link.href" target="_blank" :aria-label="link.title"> 102 - <Icon :name="link.icon" size="2rem" :title="link.title" /> 103 - </NuxtLink> 104 - </template> 105 - </div> 106 - </div> 107 - </header> 108 - <main class=".content-grid"> 109 - <NuxtPage /> 110 - </main> 111 - <footer :class="[ 112 - 'border-t-2 border-stone-200 dark:border-stone-800', 113 - 'p-4', 114 - 'flex justify-between', 115 - ]"> 116 - <p> 117 - &copy; 118 - {{ date.getFullYear() }} 119 - {{ config.author }} 120 - 121 - <span v-if="config.author !== 'finxol'" class="text-sm text-gray-500"> 122 - <span class="mx-3"> 123 - 124 - </span> 125 - Theme by <a class="underline" href="https://github.com/finxol/nuxt-blog-template" target="_blank" rel="noopener noreferrer">finxol</a> 126 - </span> 127 - </p> 128 - <button :class="['text-gray-600 dark:text-gray-300', 'font-light']" @click="scrollToTop"> 129 - Back to top 130 - </button> 131 - </footer> 132 - </div> 5 + <NuxtLayout> 6 + <NuxtPage /> 7 + </NuxtLayout> 133 8 </template> 134 9 135 10 <style>
+36
app/error.vue
··· 1 + <script setup lang="ts"> 2 + import type { NuxtError } from "#app"; 3 + 4 + const props = defineProps<{ error: NuxtError }>(); 5 + </script> 6 + 7 + <template> 8 + <NuxtLayout> 9 + <article class="flex items-center justify-center flex-col h-full gap-6 "> 10 + <template v-if="error.status === 404"> 11 + <h2 class="text-3xl font-bold text-gray-900 dark:text-gray-200"> 12 + It seems you might be lost... 13 + </h2> 14 + <p class="text-gray-500 dark:text-gray-400"> 15 + {{error.statusText}}. Please check the URL and try again. 16 + </p> 17 + 18 + <div></div> 19 + 20 + <div class="flex flex-row gap-2 items-baseline"> 21 + <NuxtLink href="/" class="text-lg"> 22 + Take me home!</NuxtLink> 23 + <span class="text-gray-500 dark:text-gray-400 text-sm">(country roads)</span> 24 + </div> 25 + </template> 26 + <template v-else> 27 + <h1>An unknown error occurred</h1> 28 + <div class="flex flex-row gap-2 items-baseline"> 29 + <NuxtLink href="/" class="text-lg"> 30 + Take me home!</NuxtLink> 31 + <span class="text-gray-500 dark:text-gray-400 text-sm">(country roads)</span> 32 + </div> 33 + </template> 34 + </article> 35 + </NuxtLayout> 36 + </template>
+133
app/layouts/default.vue
··· 1 + <script setup> 2 + import { useDark, useToggle } from "@vueuse/core"; 3 + 4 + const config = useRuntimeConfig().public; 5 + 6 + const pageBackground = ref("bg-stone-100 dark:bg-neutral-900"); 7 + 8 + const isDark = useDark(); 9 + const toggleDark = useToggle(isDark); 10 + 11 + useHead({ 12 + title: config.title, 13 + meta: [ 14 + { 15 + name: "viewport", 16 + content: "width=device-width, initial-scale=1" 17 + }, 18 + ...config.meta 19 + ], 20 + bodyAttrs: { 21 + class: pageBackground 22 + } 23 + }); 24 + 25 + const { data } = await useAsyncData("navigation", () => { 26 + return queryCollectionNavigation("pages", ["path"]); 27 + }); 28 + 29 + const pages = data.value ? data.value[0]?.children : []; 30 + 31 + const links = ref( 32 + [ 33 + { 34 + icon: "ant-design:github-filled", 35 + title: "GitHub", 36 + href: config.links.github 37 + }, 38 + { 39 + icon: "ri:mastodon-fill", 40 + title: "Mastodon", 41 + href: config.links.mastodon 42 + }, 43 + { 44 + icon: "ri:bluesky-fill", 45 + title: "BlueSky", 46 + href: config.links.bluesky 47 + } 48 + ].reverse() 49 + ); 50 + 51 + const date = new Date(); 52 + 53 + function scrollToTop() { 54 + window.scrollTo({ 55 + top: 0, 56 + behavior: "smooth" 57 + }); 58 + } 59 + </script> 60 + 61 + <template> 62 + <div :class="[ 63 + 'page-container', 64 + pageBackground, 65 + 'text-gray-800 dark:text-gray-300', 66 + 'min-h-screen max-w-4xl', 67 + 'grid grid-cols-1 grid-rows-[auto_1fr_auto]', 68 + 'mx-auto px-6', 69 + ]"> 70 + <header :class="[ 71 + 'border-b-2 border-stone-200 dark:border-stone-800', 72 + 'py-6 md:py-8', 73 + 'flex justify-between align-center', 74 + ]"> 75 + <div class="flex items-center gap-4 sm:gap-6"> 76 + <img src="/logo.png" alt="Logo" class="hidden sm:block h-8" /> 77 + <NuxtLink to="/" class="text-xl leading-5 sm:text-2xl font-medium font-serif-bold"> 78 + {{ config.title }} 79 + </NuxtLink> 80 + </div> 81 + 82 + <div class="flex items-center gap-4 sm:gap-8"> 83 + <Country /> 84 + <div 85 + class="cursor-pointer" 86 + @click="toggleDark()" 87 + > 88 + <Icon v-if="isDark" name="ri:sun-fill" size="1.5rem" mode="svg" /> 89 + <Icon v-else name="ri:moon-fill" size="1.5rem" mode="svg" /> 90 + </div> 91 + <nav :class="['flex items-start gap-4', 'text-gray-800 dark:text-gray-200', 'font-semibold']"> 92 + <template v-for="item in pages" :key="item.path"> 93 + <NuxtLink v-if="item.title" :to="item.path.replace('/pages', '')"> 94 + {{ item.title }} 95 + </NuxtLink> 96 + </template> 97 + </nav> 98 + 99 + <div class="flex items-center gap-2"> 100 + <template v-for="link in links" :key="link.title"> 101 + <NuxtLink v-if="link.href" :to="link.href" target="_blank" :aria-label="link.title"> 102 + <Icon :name="link.icon" size="2rem" :title="link.title" /> 103 + </NuxtLink> 104 + </template> 105 + </div> 106 + </div> 107 + </header> 108 + <main class=".content-grid"> 109 + <slot /> 110 + </main> 111 + <footer :class="[ 112 + 'border-t-2 border-stone-200 dark:border-stone-800', 113 + 'p-4', 114 + 'flex justify-between', 115 + ]"> 116 + <p> 117 + &copy; 118 + {{ date.getFullYear() }} 119 + {{ config.author }} 120 + 121 + <span v-if="config.author !== 'finxol'" class="text-sm text-gray-500"> 122 + <span class="mx-3"> 123 + 124 + </span> 125 + Theme by <a class="underline" href="https://github.com/finxol/nuxt-blog-template" target="_blank" rel="noopener noreferrer">finxol</a> 126 + </span> 127 + </p> 128 + <button :class="['text-gray-600 dark:text-gray-300', 'font-light']" @click="scrollToTop"> 129 + Back to top 130 + </button> 131 + </footer> 132 + </div> 133 + </template>
+4 -15
app/pages/[...page].vue
··· 6 6 queryCollection("pages").path(`/pages${route.path}`).first() 7 7 ); 8 8 9 + if (!page.value) { 10 + throw createError({ statusCode: 404, statusMessage: "Page not found" }); 11 + } 12 + 9 13 useSeoMeta({ 10 14 title: page.value?.title, 11 15 description: page.value?.description ··· 31 35 'mt-6', 32 36 ]" 33 37 /> 34 - </div> 35 - 36 - <div v-else> 37 - <article class="grid place-items-center gap-6 mt-12"> 38 - <h2 class="text-3xl font-bold text-gray-900 dark:text-gray-200">It seems you might be lost...</h2> 39 - <p class="text-gray-500 dark:text-gray-400">Please check the URL and try again.</p> 40 - 41 - <div></div> 42 - 43 - <div class="flex flex-row gap-2 items-baseline"> 44 - <A href="/" target="_self" class="text-lg"> 45 - Take me home!</A> 46 - <span class="text-gray-500 dark:text-gray-400 text-sm">(country roads)</span> 47 - </div> 48 - </article> 49 38 </div> 50 39 </template>
+8
app/pages/posts/[...slug].vue
··· 8 8 queryCollection("posts").path(route.path).first() 9 9 ); 10 10 11 + if (!post.value) { 12 + throw createError({ 13 + statusCode: 404, 14 + statusMessage: "Post not found", 15 + fatal: true 16 + }); 17 + } 18 + 11 19 useSeoMeta({ 12 20 title: post.value?.title, 13 21 description: post.value?.description