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