Skip to content

Commit af16d6e

Browse files
committed
feat: enable server-side rendering for navigation, social media, and blog pages; add blog pagination routes
1 parent b5af0c4 commit af16d6e

File tree

8 files changed

+247
-24
lines changed

8 files changed

+247
-24
lines changed

app/composables/useNavigation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export function useNavigation() {
1818
hotCtaText,
1919

2020
}
21+
}, {
22+
server: true,
2123
})
2224
}
2325

app/composables/useSocialMedias.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export function useSocialMedias() {
5353
) as Record<SocialMediaName, SocialMediaAttributes>
5454
return socialMedias
5555
}, {
56+
server: true,
5657
default() {
5758
return {} as Record<SocialMediaName, SocialMediaAttributes>
5859
},

app/pages/[...uid].vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ const { data: page } = await useAsyncData(`prismic-page-${pathParams.join('-')}`
1515
.catch((error) => {
1616
console.error(`Page with UID "${uid}" not found in Prismic:`, error)
1717
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
18-
}))
18+
}), {
19+
server: true,
20+
})
1921
2022
if (!page.value) {
2123
console.error(`Page with UID "${uid}" not found`)

app/pages/blog/[post].vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import { components } from '~/slices'
55
const postSlug = useRouteParams<string>('post')
66
77
const { client } = usePrismic()
8-
const { data: post } = await useAsyncData(`prismic-post-${postSlug.value}`, () => client.getByUID('blog_page', postSlug.value)
9-
.catch((error) => {
8+
const { data: post } = await useAsyncData(`blog-post-${postSlug.value}`, async () => {
9+
try {
10+
return await client.getByUID('blog_page', postSlug.value)
11+
}
12+
catch (error) {
1013
console.error(`Blog post with slug "${postSlug.value}" not found:`, error)
1114
throw createError({ statusCode: 404, statusMessage: 'Article not found', fatal: true })
12-
}))
15+
}
16+
}, {
17+
server: true,
18+
})
1319
1420
const { showDrafts } = useRuntimeConfig().public
1521

app/pages/blog/index.vue

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<script setup lang="ts">
2+
import type { BlogPageDocument } from '~~/prismicio-types'
3+
import { filter } from '@prismicio/client'
4+
import { getBlogMetadata } from '~~/shared/utils/blog-post'
5+
6+
const route = useRoute()
7+
const router = useRouter()
8+
const page = ref(Number(route.query.page) || 1)
9+
10+
// Configuration
11+
const itemsPerPage = 25
12+
const { showDrafts } = useRuntimeConfig().public
13+
14+
// Watch for query param changes
15+
watch(() => route.query.page, (val) => {
16+
page.value = Number(val) || 1
17+
})
18+
19+
// Update URL when page changes
20+
watch(page, (val) => {
21+
router.replace({ query: { ...route.query, page: val !== 1 ? val : undefined } })
22+
if (import.meta.client) {
23+
window.scrollTo({ top: 0, behavior: 'smooth' })
24+
}
25+
})
26+
27+
const { client } = usePrismic()
28+
const { data: result } = await useAsyncData(`blog-posts-page-${page.value}`, async () => {
29+
return await client.getByType('blog_page', {
30+
orderings: { field: 'my.blog_page.publish_date', direction: 'desc' },
31+
filters: showDrafts ? undefined : [filter.not('my.blog_page.draft', true)],
32+
pageSize: itemsPerPage,
33+
page: page.value,
34+
})
35+
}, {
36+
server: true,
37+
watch: [page],
38+
})
39+
40+
const posts = computed(() => {
41+
if (!result.value?.results)
42+
return []
43+
return result.value.results.map(r => getBlogMetadata(r as BlogPageDocument))
44+
})
45+
46+
const totalPages = computed(() => result.value?.total_pages ?? 1)
47+
48+
// SEO
49+
useHead({
50+
title: page.value === 1 ? 'Blog' : `Blog - Page ${page.value}`,
51+
meta: [
52+
{ name: 'description', content: 'Latest articles and insights from the Nimiq team' },
53+
],
54+
})
55+
</script>
56+
57+
<template>
58+
<div>
59+
<header py-32>
60+
<h1 text-4xl font-bold text-center>
61+
Blog
62+
</h1>
63+
<p text-muted-foreground mt-4 text-center>
64+
Latest articles and insights from the Nimiq team
65+
</p>
66+
</header>
67+
68+
<main>
69+
<div v-if="posts.length > 0" grid="~ cols-1 lg:cols-2 xl:cols-3 gap-16" mx-auto px-16 w-full max-w-screen-xl>
70+
<article
71+
v-for="({ uid, href, draft, image, hasImage, title, abstract, date, authors }, i) in posts"
72+
:key="uid"
73+
:class="page === 1 ? { 'md:first:col-span-2': true } : 'self-stretch'"
74+
>
75+
<NuxtLink :to="href" p-0 h-full relative nq-hoverable>
76+
<PageInfo :draft right-12 top-12 absolute z-10 />
77+
<div p-4>
78+
<PrismicImage
79+
v-if="hasImage"
80+
:field="image"
81+
rounded-6
82+
h-max
83+
w-full
84+
object-cover
85+
:class="[i === 0 ? 'h-max lg:h-280' : 'h-max']"
86+
loading="lazy"
87+
/>
88+
<div v-else-if="showDrafts" text-green-400 py-64 rounded-4 flex-1 size-full bg-gradient-green grid="~ place-content-center">
89+
<div flex="~ items-center gap-12">
90+
<div text-32 op-70 i-nimiq:tools-wench-hammer />
91+
<p font-bold f-text-xl>
92+
Image not found
93+
</p>
94+
</div>
95+
<p font-semibold mt-8 op-80 max-w-40ch>
96+
Something great is being redacted just right now and there is no image yet. 🤫
97+
</p>
98+
<p op-70 italic f-text-2xs f-mt-2xs>
99+
This is a development-only message.
100+
</p>
101+
</div>
102+
</div>
103+
<div flex="~ col" p-24 h-full>
104+
<PrismicRichText
105+
wrapper="h2"
106+
text-left
107+
:field="title"
108+
:class="{ 'f-text-3xl': i === 0, 'f-text-2xl': i === 1, 'f-text-xl': i > 1 }"
109+
/>
110+
<p mt-8 line-clamp-2 text="16 neutral-900 left">
111+
{{ abstract }}
112+
</p>
113+
<ArticleMetadata
114+
style="--content: 'Learn more'"
115+
:class="i === 1 ? 'mt-4' : 'mt-auto'"
116+
after="text-blue content-$content text-16"
117+
:date
118+
:authors="authors.join(', ')"
119+
pt-16
120+
gap-x-8
121+
h-max
122+
nq-hoverable-cta
123+
/>
124+
<span sr-only>Learn more</span>
125+
</div>
126+
</NuxtLink>
127+
</article>
128+
</div>
129+
130+
<!-- Pagination -->
131+
<nav v-if="totalPages > 1" aria-label="Blog pagination" py-32>
132+
<PaginationRoot v-model:page="page" :total="totalPages * itemsPerPage" :items-per-page="itemsPerPage" show-edges>
133+
<PaginationList v-slot="{ items }" flex="~ gap-16 items-center justify-center">
134+
<PaginationPrev class="item">
135+
<div text-9 op-70 i-nimiq:chevron-left />
136+
</PaginationPrev>
137+
<template v-for="(pageItem, index) in items">
138+
<PaginationListItem v-if="pageItem.type === 'page'" :key="index" class="item" :value="pageItem.value">
139+
{{ pageItem.value }}
140+
</PaginationListItem>
141+
<PaginationEllipsis v-else :key="pageItem.type" :index="index" class="item">
142+
&#8230;
143+
</PaginationEllipsis>
144+
</template>
145+
<PaginationNext class="item">
146+
<div text-9 op-70 i-nimiq:chevron-right />
147+
</PaginationNext>
148+
</PaginationList>
149+
</PaginationRoot>
150+
</nav>
151+
</main>
152+
</div>
153+
</template>
154+
155+
<style scoped>
156+
.item {
157+
--uno: 'rounded-4 size-32 shrink-0 bg-neutral-100 text-neutral-900 text-12 font-semibold hocus:bg-neutral-200 transition-colors ring-1.5 ring-neutral-400 flex items-center justify-center';
158+
&[data-selected] {
159+
--uno: 'bg-blue text-white ring-none';
160+
}
161+
}
162+
</style>

app/slices/BlogpostsGrid/index.vue

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,71 @@
11
<script setup lang="ts">
2-
import type { Content, Query } from '@prismicio/client'
2+
import type { Content } from '@prismicio/client'
3+
import type { BlogPageDocument } from '~~/prismicio-types'
34
import { filter } from '@prismicio/client'
45
import ArticleMetadata from '~/components/ArticleMetadata.vue'
56
6-
defineProps(getSliceComponentProps<Content.BlogpostsGridSlice>())
7+
const { slice } = defineProps(getSliceComponentProps<Content.BlogpostsGridSlice>())
78
8-
const { showDrafts } = useRuntimeConfig().public
99
const itemsPerPage = 25
1010
const route = useRoute()
1111
const router = useRouter()
1212
const page = ref(Number(route.query.page) || 1)
13+
const { showDrafts } = useRuntimeConfig().public
1314
1415
watch(() => route.query.page, (val) => {
1516
page.value = Number(val) || 1
1617
})
1718
1819
watch(page, (val) => {
1920
router.replace({ query: { ...route.query, page: val !== 1 ? val : undefined } })
20-
window.scrollTo({ top: 0, behavior: 'smooth' })
21+
if (import.meta.client) {
22+
window.scrollTo({ top: 0, behavior: 'smooth' })
23+
}
2124
})
2225
2326
const { client } = usePrismic()
24-
25-
const result = ref<Query<Content.BlogPageDocument<string>>>()
26-
27-
watchEffect(async () => {
28-
const data = await client.getByType('blog_page', {
27+
const { data: result } = await useAsyncData(`blog-posts-slice-${page.value}`, async () => {
28+
return await client.getByType('blog_page', {
2929
orderings: { field: 'my.blog_page.publish_date', direction: 'desc' },
3030
filters: showDrafts ? undefined : [filter.not('my.blog_page.draft', true)],
3131
pageSize: itemsPerPage,
3232
page: page.value,
3333
})
34-
result.value = data
34+
}, {
35+
server: true,
36+
watch: [page],
37+
})
38+
39+
const posts = computed(() => {
40+
if (!result.value?.results)
41+
return []
42+
return result.value.results.map(r => getBlogMetadata(r as BlogPageDocument))
3543
})
3644
37-
const results = computed(() => result.value?.results ?? [])
3845
const totalPages = computed(() => result.value?.total_pages ?? 1)
39-
const posts = computed(() => results.value.map(r => getBlogMetadata(r as Content.BlogPageDocument)))
4046
4147
const active = useState()
4248
</script>
4349

4450
<template>
4551
<section bg-neutral-100 f-pt-3xl>
4652
<div v-if="posts.length > 0" grid="~ cols-1 lg:cols-2 xl:cols-3 gap-16" w-full>
47-
<article v-for="({ uid, href, draft, image, hasImage, title, abstract, date, authors }, i) in posts" :key="uid" :class="page === 1 ? { 'md:first:col-span-2': true } : 'self-stretch'">
53+
<article
54+
v-for="({ uid, href, draft, image, hasImage, title, abstract, date, authors }, i) in posts" :key="uid"
55+
:class="page === 1 ? { 'md:first:col-span-2': true } : 'self-stretch'"
56+
>
4857
<NuxtLink :to="href" p-0 h-full relative nq-hoverable @click="active = uid">
4958
<PageInfo :draft right-12 top-12 absolute z-10 />
5059
<div p-4>
51-
<PrismicImage v-if="hasImage" :field="image" rounded-6 h-max w-full object-cover :class="[i === 1 ? 'h-max lg:h-280' : 'h-max', { 'view-transition-post-img contain-layout': active === uid }]" loading="lazy" />
52-
<div v-else-if="showDrafts" text-green-400 py-64 rounded-4 flex-1 size-full bg-gradient-green grid="~ place-content-center">
60+
<PrismicImage
61+
v-if="hasImage" :field="image" rounded-6 h-max w-full object-cover
62+
:class="[i === 1 ? 'h-max lg:h-280' : 'h-max', { 'view-transition-post-img contain-layout': active === uid }]"
63+
loading="lazy"
64+
/>
65+
<div
66+
v-else-if="showDrafts" text-green-400 py-64 rounded-4 flex-1 size-full bg-gradient-green
67+
grid="~ place-content-center"
68+
>
5369
<div flex="~ items-center gap-12">
5470
<div text-32 op-70 i-nimiq:tools-wench-hammer />
5571

@@ -74,12 +90,19 @@ const active = useState()
7490
<p mt-8 line-clamp-2 text="16 neutral-900 left">
7591
{{ abstract }}
7692
</p>
77-
<ArticleMetadata :style="`--content: '${slice.primary.labelLearnMore}'`" :class=" i === 1 ? 'mt-4' : 'mt-auto'" after="text-blue content-$content text-16" :date :authors="authors.join(', ')" pt-16 gap-x-8 h-max nq-hoverable-cta />
93+
<ArticleMetadata
94+
:style="`--content: '${slice.primary.labelLearnMore}'`"
95+
:class="i === 1 ? 'mt-4' : 'mt-auto'" after="text-blue content-$content text-16" :date
96+
:authors="authors.join(', ')" pt-16 gap-x-8 h-max nq-hoverable-cta
97+
/>
7898
<span sr-only>{{ slice.primary.labelLearnMore }}</span>
7999
</div>
80100
</NuxtLink>
81101
</article>
82-
<PaginationRoot v-model:page="page" :total="totalPages * itemsPerPage" :items-per-page show-edges mt-32 col-span-full>
102+
<PaginationRoot
103+
v-model:page="page" :total="totalPages * itemsPerPage" :items-per-page show-edges mt-32
104+
col-span-full
105+
>
83106
<PaginationList v-slot="{ items }" flex="~ gap-16 items-center justify-center">
84107
<PaginationPrev class="item">
85108
<div text-9 op-70 i-nimiq:chevron-left />
@@ -104,6 +127,7 @@ const active = useState()
104127
<style scoped>
105128
.item {
106129
--uno: 'rounded-4 size-32 shrink-0 bg-neutral-100 text-neutral-900 text-12 font-semibold hocus:bg-neutral-200 transition-colors ring-1.5 ring-neutral-400 flex items-center justify-center';
130+
107131
&[data-selected] {
108132
--uno: 'bg-blue text-white ring-none';
109133
}

app/slices/NewsletterForm/index.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const topics = computed(() => nimiqTopicsOptions.filter(topic => topic.model).ma
1919
const products = computed(() => nimiqProducts.filter(product => product.model).map(product => product.label))
2020
const body = computed(() => ({ email: email.value, communicationPermission: communicationPermission.value, topics: topics.value, products: products.value }))
2121
22-
const url = `${useRuntimeConfig().public.apiDomain}/api/newsletter/subscribe`
23-
const { execute: submitForm, status, error } = useFetch(url, { method: 'POST', body, watch: false, immediate: false })
22+
const url = new URL('/api/newsletter/subscribe', useRuntimeConfig().public.apiDomain)
23+
const { execute: submitForm, status, error } = useFetch(url.href, { method: 'POST', body, watch: false, immediate: false })
2424
</script>
2525

2626
<template>

lib/crawler.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ export async function getDynamicPages(options: PrerenderPagesOptions) {
2828
const blogPostsUrl = await buildPrismicUrl('blog_page', options)
2929
const blogArticles = await getBlogPosts(blogPostsUrl).then(posts => posts.map(post => `/blog/${post.slug}`))
3030

31-
return [...pages, ...blogArticles].filter(page => !EXCLUDED_PAGES.includes(page))
31+
// Generate blog pagination routes
32+
const blogPaginationRoutes = await getBlogPaginationRoutes(blogPostsUrl)
33+
34+
return [...pages, ...blogArticles, ...blogPaginationRoutes].filter(page => !EXCLUDED_PAGES.includes(page))
3235
}
3336

3437
async function getPages(url: URL) {
@@ -87,6 +90,29 @@ export async function getBlogPosts(url: URL) {
8790
return blogPosts
8891
}
8992

93+
export async function getBlogPaginationRoutes(url: URL) {
94+
const paginationRoutes: string[] = []
95+
96+
// Get the first page to determine total number of posts
97+
url.searchParams.set('page', '1')
98+
const firstPageResult = await $fetch<Query<BlogPageDocument>>(url.href)
99+
const totalPages = firstPageResult.total_pages || 1
100+
101+
// Generate pagination routes
102+
for (let i = 1; i <= totalPages; i++) {
103+
if (i === 1) {
104+
// First page is just /blog
105+
paginationRoutes.push('/blog')
106+
}
107+
else {
108+
// Other pages are /blog?page=2, /blog?page=3, etc.
109+
paginationRoutes.push(`/blog?page=${i}`)
110+
}
111+
}
112+
113+
return paginationRoutes
114+
}
115+
90116
const prismicUrl = new URL(`https://${repositoryName}.cdn.prismic.io`)
91117

92118
interface RefsResponse { refs: { id: 'master', ref: string }[] }

0 commit comments

Comments
 (0)