blog pagination & sitemap
This commit is contained in:
parent
ed756d0646
commit
74d93541a3
6
.env
6
.env
|
@ -1,6 +1,6 @@
|
|||
NEXT_PUBLIC_SERVER_BASE_URL=https://api.bbuddy.expert/api
|
||||
NEXT_PUBLIC_AGORA_APPID=ed90c9dc42634e5687d4e2e0766b363f
|
||||
|
||||
CONTENTFUL_SPACE_ID = voxpxjq7y7vf
|
||||
CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
|
||||
CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE
|
||||
NEXT_PUBLIC_CONTENTFUL_SPACE_ID = voxpxjq7y7vf
|
||||
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN = s99GWKfpDKkNwiEJ3pN7US_tmqsGvDlaex-sOJwpzuc
|
||||
NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN = Z9WOKpLDbKNj7xVOmT_VXYNLH0AZwISFvQsq0PQlHfE
|
||||
|
|
|
@ -6,11 +6,6 @@ import {fetchBlogPost, fetchBlogPosts, Widget} from "../../../../lib/contentful/
|
|||
import Util from "node:util";
|
||||
import RichText from "../../../../lib/contentful/RichText";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Bbuddy - Blog item',
|
||||
description: 'Bbuddy desc blog item'
|
||||
};
|
||||
|
||||
interface BlogPostPageParams {
|
||||
slug: string
|
||||
}
|
||||
|
@ -19,11 +14,19 @@ interface BlogPostPageProps {
|
|||
params: BlogPostPageParams
|
||||
}
|
||||
|
||||
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
|
||||
const blogPosts = await fetchBlogPosts({ preview: false })
|
||||
export async function generateMetadata({ params }: BlogPostPageProps, parent: ResolvingMetadata): Promise<Metadata> {
|
||||
const blogPost = await fetchBlogPost({ slug: params.slug, preview: draftMode().isEnabled })
|
||||
|
||||
return blogPosts.map((post) => ({ slug: post.slug }))
|
||||
if (!blogPost) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return {
|
||||
title: blogPost.title
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderWidget (widget: Widget) {
|
||||
switch (widget.type){
|
||||
case 'widgetParagraph':
|
||||
|
|
|
@ -5,6 +5,7 @@ import {unstable_setRequestLocale} from "next-intl/server";
|
|||
import Link from "next/link";
|
||||
import {fetchBlogPosts} from "../../../../../lib/contentful/blogPosts";
|
||||
import {fetchBlogPostCategories} from "../../../../../lib/contentful/blogPostsCategories";
|
||||
import {BlogPosts} from "../../../../../components/BlogPosts/BlogPosts";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Bbuddy - Blog',
|
||||
|
@ -19,83 +20,10 @@ interface BlogPostPageProps {
|
|||
params: BlogPostPageParams
|
||||
}
|
||||
|
||||
export default async function Blog({params}: { params: BlogPostPageParams }) {
|
||||
export default async function Blog({params, searchParams}: { params: BlogPostPageParams, searhParams?: {page: number} }) {
|
||||
unstable_setRequestLocale(params.locale);
|
||||
const data = await fetchBlogPosts({ preview: draftMode().isEnabled, locale: params.locale, category: params.slug })
|
||||
const cats = await fetchBlogPostCategories(false)
|
||||
const page = searchParams.page || undefined
|
||||
return (
|
||||
<div className="b-news">
|
||||
<div className="b-news__header">
|
||||
<div className="b-inner">
|
||||
<h1 className="title-h1">
|
||||
Mentorship, Career <br/>
|
||||
Development & Coaching.
|
||||
</h1>
|
||||
<div className="wrap-text">
|
||||
<p className="">The ins-and-outs of building a career in tech, gaining <br/> experience</p>
|
||||
<p className="">from a mentor, and getting your feet wet with coaching.</p>
|
||||
</div>
|
||||
<div className="b-news__header__img">
|
||||
<img className="" src="/images/news-top.png" alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="b-news__filter ">
|
||||
<div className="b-inner">
|
||||
<div className="wrap-filter">
|
||||
{
|
||||
cats.map((cat, i)=>(
|
||||
<Link key={'blogCat'+i} href={'/'+params.locale+'/blog/category/'+cat.slug} className={"filter-item"+(cat.slug === params.slug ? ' active' : '')}>{cat.title}</Link>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="b-news__result-list">
|
||||
<div className="b-inner">
|
||||
<div className="news-list">
|
||||
{data.map((item, i) => (
|
||||
<li key={'blogPost'+i} className="list-sidebar__item">
|
||||
<Link href={'/'+params.locale+'/blog/'+item.slug} className="news-item">
|
||||
<div className="news-item__image">
|
||||
<img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
|
||||
</div>
|
||||
<div className="news-item__inner">
|
||||
<div className="">
|
||||
<div className="news-item__title">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="news-item__badge">{item.category}</div>
|
||||
<div className="news-item__text">
|
||||
{item.excerpt}
|
||||
</div>
|
||||
</div>
|
||||
<div className="news-item__info">
|
||||
<div className="news-item__info__author">
|
||||
<img className="" src={item.author.avatar.src} alt=""/>
|
||||
<div className="news-item__info__author__inner">
|
||||
<div className="news-item__info__name">{item.author.name}</div>
|
||||
<div className="news-item__info__date">{item.createdAt}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="news-item__info__counter">
|
||||
<div className="news-item__info__like">
|
||||
<img className="" src="/images/heart-outline.svg" alt=""/>
|
||||
165
|
||||
</div>
|
||||
<div className="news-item__info__share">
|
||||
<img className="" src="/images/share-social.svg" alt=""/>
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BlogPosts basePath={'/'+params.locale+'/blog/'} locale={params.locale} currentCat={params.slug} page={page}/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,91 +5,39 @@ import {fetchBlogPosts} from "../../../lib/contentful/blogPosts";
|
|||
import {unstable_setRequestLocale} from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import {fetchBlogPostCategories} from "../../../lib/contentful/blogPostsCategories";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Bbuddy - Blog',
|
||||
description: 'Bbuddy desc blog'
|
||||
};
|
||||
import {CustomPagination} from "../../../components/view/CustomPagination";
|
||||
import {DEFAULT_PAGE_SIZE} from "../../../constants/common";
|
||||
import {BlogPosts} from "../../../components/BlogPosts/BlogPosts";
|
||||
|
||||
|
||||
|
||||
export default async function Blog({ params: { locale } }: { params: { locale: string } }) {
|
||||
unstable_setRequestLocale(locale);
|
||||
const data = await fetchBlogPosts(false, locale)
|
||||
const cats = await fetchBlogPostCategories(false)
|
||||
return (
|
||||
<div className="b-news">
|
||||
<div className="b-news__header">
|
||||
<div className="b-inner">
|
||||
<h1 className="title-h1">
|
||||
Mentorship, Career <br/>
|
||||
Development & Coaching.
|
||||
</h1>
|
||||
<div className="wrap-text">
|
||||
<p className="">The ins-and-outs of building a career in tech, gaining <br/> experience</p>
|
||||
<p className="">from a mentor, and getting your feet wet with coaching.</p>
|
||||
</div>
|
||||
<div className="b-news__header__img">
|
||||
<img className="" src="/images/news-top.png" alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="b-news__filter ">
|
||||
<div className="b-inner">
|
||||
<div className="wrap-filter">
|
||||
{
|
||||
cats.map((cat, i)=>(
|
||||
<Link href={'category/'+cat.slug} className="filter-item">{cat.title}</Link>
|
||||
))
|
||||
interface BlogPostPageParams {
|
||||
slug: string
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="b-news__result-list">
|
||||
<div className="b-inner">
|
||||
<div className="news-list">
|
||||
{data.map((item, i) => (
|
||||
<li key={'blog'+i} className="list-sidebar__item">
|
||||
<Link href={`${item.slug}`} className="news-item">
|
||||
<div className="news-item__image">
|
||||
<img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
|
||||
</div>
|
||||
<div className="news-item__inner">
|
||||
<div className="">
|
||||
<div className="news-item__title">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="news-item__badge">{item.category}</div>
|
||||
<div className="news-item__text">
|
||||
{item.excerpt}
|
||||
</div>
|
||||
</div>
|
||||
<div className="news-item__info">
|
||||
<div className="news-item__info__author">
|
||||
<img className="" src={item.author.avatar.src} alt=""/>
|
||||
<div className="news-item__info__author__inner">
|
||||
<div className="news-item__info__name">{item.author.name}</div>
|
||||
<div className="news-item__info__date">{item.createdAt}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="news-item__info__counter">
|
||||
<div className="news-item__info__like">
|
||||
<img className="" src="/images/heart-outline.svg" alt=""/>
|
||||
165
|
||||
</div>
|
||||
<div className="news-item__info__share">
|
||||
<img className="" src="/images/share-social.svg" alt=""/>
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
interface BlogPostPageProps {
|
||||
params: BlogPostPageParams
|
||||
}
|
||||
|
||||
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
|
||||
const blogPosts = await fetchBlogPosts({ preview: false })
|
||||
|
||||
return blogPosts.data.map((post) => ({ slug: post.slug }))
|
||||
}
|
||||
|
||||
|
||||
export default async function Blog({ params: { locale }, searchParams }: { params: { locale: string }, searhParams?: {page: number} }) {
|
||||
unstable_setRequestLocale(locale);
|
||||
const pageSize = DEFAULT_PAGE_SIZE
|
||||
const page = searchParams.page || undefined
|
||||
// BlogPosts('/'+locale+'/blog/', locale, pageSize)
|
||||
return (
|
||||
|
||||
<BlogPosts
|
||||
basePath={'/'+locale+'/blog/'}
|
||||
locale={locale}
|
||||
pageSize={pageSize}
|
||||
page={page}
|
||||
>
|
||||
</BlogPosts>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import {fetchBlogPosts} from "../lib/contentful/blogPosts";
|
||||
|
||||
export default async function sitemap() {
|
||||
const paths = [
|
||||
{
|
||||
url: process.env.NEXT_PUBLIC_HOST,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "monthly",
|
||||
priority: 1
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const blogPosts = await fetchBlogPosts({ preview: false })
|
||||
|
||||
blogPosts.data.forEach((item) => {
|
||||
|
||||
paths.push({
|
||||
url: `${process.env.NEXT_PUBLIC_HOST}${item.slug}`,
|
||||
lastModified: item.createdAt.split('T')[0],
|
||||
changeFrequency: 'daily',
|
||||
priority: '1.0'
|
||||
})
|
||||
})
|
||||
|
||||
return paths
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
'use client';
|
||||
import React, {useState} from "react";
|
||||
import {Languages} from "../../types/tags";
|
||||
import {BlogPostCategory} from "../../types/blogPostCategory";
|
||||
import Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
languages?: Languages;
|
||||
basePath: string;
|
||||
locale: string;
|
||||
cats: BlogPostCategory[],
|
||||
slug: string
|
||||
};
|
||||
|
||||
export const BlogPostCategories = ({ basePath = '/', cats = [], slug = '' }: Props) => {
|
||||
const [currentCat, setCurrentCat] = useState<String>(slug);
|
||||
return (
|
||||
<div className="b-news__filter ">
|
||||
<div className="b-inner">
|
||||
<div className="wrap-filter">
|
||||
{
|
||||
cats.map((cat, i)=>(
|
||||
<Link
|
||||
href={ basePath+'category/'+cat.slug} key={'blogCat'+i}
|
||||
className={"filter-item"+(cat.slug === currentCat ? ' active' : '')}
|
||||
>
|
||||
{cat.title}
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import { DEFAULT_PAGE_SIZE } from '../../constants/common';
|
||||
import {getLanguages} from "../../actions/tags";
|
||||
import {fetchBlogPosts} from "../../lib/contentful/blogPosts";
|
||||
import {fetchBlogPostCategories} from "../../lib/contentful/blogPostsCategories";
|
||||
import {BlogPostsList} from "./BlogPostsList";
|
||||
import {BlogPostCategories} from "./BlogPostCategories";
|
||||
|
||||
type PostsProps = {
|
||||
basePath: string;
|
||||
locale: string;
|
||||
pageSize?: number;
|
||||
currentCat: string;
|
||||
page?: number
|
||||
};
|
||||
|
||||
export const BlogPosts = async ({ basePath = '/', locale, pageSize = DEFAULT_PAGE_SIZE, currentCat = '', page = 1 }: PostsProps) => {
|
||||
const languages = await getLanguages(locale);
|
||||
const {data, total} = await fetchBlogPosts({preview: false, category: currentCat, page: page})
|
||||
const cats = await fetchBlogPostCategories(false)
|
||||
|
||||
return (
|
||||
<div className="b-news">
|
||||
<div className="b-news__header">
|
||||
<div className="b-inner">
|
||||
<h1 className="title-h1">
|
||||
Mentorship, Career <br/>
|
||||
Development & Coaching
|
||||
</h1>
|
||||
<div className="wrap-text">
|
||||
<p className="">The ins-and-outs of building a career in tech, gaining <br/> experience</p>
|
||||
<p className="">from a mentor, and getting your feet wet with coaching.</p>
|
||||
</div>
|
||||
<div className="b-news__header__img">
|
||||
<img className="" src="/images/news-top.png" alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BlogPostCategories
|
||||
slug={currentCat}
|
||||
cats={cats}
|
||||
basePath={basePath}
|
||||
locale={locale}
|
||||
/>
|
||||
<BlogPostsList
|
||||
data={data}
|
||||
total={total}
|
||||
basePath={basePath}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { DEFAULT_PAGE_SIZE } from '../../constants/common';
|
||||
import {Languages, SearchData} from "../../types/tags";
|
||||
import {BlogPost} from "../../types/blogPost";
|
||||
import Link from "next/link";
|
||||
import {CustomPagination} from "../view/CustomPagination";
|
||||
|
||||
type Props = {
|
||||
searchData?: SearchData;
|
||||
languages?: Languages;
|
||||
basePath: string;
|
||||
locale: string;
|
||||
data: BlogPost[],
|
||||
total: number
|
||||
};
|
||||
|
||||
export const BlogPostsList = ({ basePath = '/', locale, pageSize = DEFAULT_PAGE_SIZE, data = [], total= 0 }: Props) => {
|
||||
const currentPage = 1
|
||||
const onChangePage = (page: number) => {
|
||||
router.push(page === 1 ? basePath : basePath+'?page='+page);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="b-news__result-list">
|
||||
<div className="b-inner">
|
||||
<div className="news-list">
|
||||
{data.map((item, i) => (
|
||||
<li key={'blog'+i} className="list-sidebar__item">
|
||||
<Link href={`${item.slug}`} className="news-item">
|
||||
<div className="news-item__image">
|
||||
<img className="" src={item.listImage?.src} alt={item.listImage?.alt}/>
|
||||
</div>
|
||||
<div className="news-item__inner">
|
||||
<div className="">
|
||||
<div className="news-item__title">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="news-item__badge">{item.category}</div>
|
||||
<div className="news-item__text">
|
||||
{item.excerpt}
|
||||
</div>
|
||||
</div>
|
||||
<div className="news-item__info">
|
||||
<div className="news-item__info__author">
|
||||
<img className="" src={item.author.avatar.src} alt=""/>
|
||||
<div className="news-item__info__author__inner">
|
||||
<div className="news-item__info__name">{item.author.name}</div>
|
||||
<div className="news-item__info__date">{item.createdAt}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="news-item__info__counter">
|
||||
<div className="news-item__info__like">
|
||||
<img className="" src="/images/heart-outline.svg" alt=""/>
|
||||
165
|
||||
</div>
|
||||
<div className="news-item__info__share">
|
||||
<img className="" src="/images/share-social.svg" alt=""/>
|
||||
Share
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
{total > pageSize && (
|
||||
<CustomPagination
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={onChangePage}
|
||||
current={currentPage}
|
||||
/>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -8,7 +8,9 @@ import {WidgetMedia, WidgetMediaEntry} from "../../types/blogWidgets/widgetMedia
|
|||
import {WidgetParagraph} from "../../types/blogWidgets/widgetParagraph";
|
||||
import entry from "next/dist/server/typescript/rules/entry";
|
||||
import Util from "node:util";
|
||||
import {DEFAULT_PAGE_SIZE} from "../../constants/common";
|
||||
|
||||
const pageSize = DEFAULT_PAGE_SIZE
|
||||
type PostEntry = BlogPostEntry<undefined, string>//Entry<BlogPostSkeleton, undefined, string>
|
||||
type widgetEnum = WidgetParagraph | WidgetMedia
|
||||
export type Widget = {
|
||||
|
@ -65,8 +67,12 @@ interface FetchBlogPostsOptions {
|
|||
preview: boolean
|
||||
local?: string
|
||||
category?: string
|
||||
page?: number
|
||||
}
|
||||
export async function fetchBlogPosts({ preview, category }: FetchBlogPostsOptions): Promise<BlogPost[]> {
|
||||
export async function fetchBlogPosts({ preview, category, page }: FetchBlogPostsOptions): Promise<{
|
||||
total: number;
|
||||
data: BlogPost[]
|
||||
}> {
|
||||
const contentful = contentfulClient({ preview })
|
||||
const query = {
|
||||
content_type: 'blogPost',
|
||||
|
@ -77,9 +83,19 @@ export async function fetchBlogPosts({ preview, category }: FetchBlogPostsOption
|
|||
query['fields.category.fields.slug'] = category
|
||||
query['fields.category.sys.contentType.sys.id']='blogPostCategory'
|
||||
}
|
||||
|
||||
if(page){
|
||||
query['limit'] = pageSize
|
||||
query['skip'] = pageSize * (page - 1)
|
||||
}
|
||||
|
||||
const blogPostsResult = await contentful.getEntries<BlogPostSkeleton>(query)
|
||||
|
||||
return blogPostsResult.items.map((blogPostEntry) => parseContentfulBlogPost(blogPostEntry) as BlogPost)
|
||||
const data = blogPostsResult.items.map((blogPostEntry) => parseContentfulBlogPost(blogPostEntry) as BlogPost)
|
||||
return {
|
||||
total: blogPostsResult.total,
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
interface FetchBlogPostOptions {
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { createClient } from 'contentful'
|
||||
|
||||
const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_PREVIEW_ACCESS_TOKEN } = process.env
|
||||
const { NEXT_PUBLIC_CONTENTFUL_SPACE_ID, NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN, NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN } = process.env
|
||||
|
||||
// This is the standard Contentful client. It fetches
|
||||
// content that has been published.
|
||||
const client = createClient({
|
||||
space: CONTENTFUL_SPACE_ID!,
|
||||
accessToken: CONTENTFUL_ACCESS_TOKEN!,
|
||||
space: NEXT_PUBLIC_CONTENTFUL_SPACE_ID!,
|
||||
accessToken: NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN!,
|
||||
})
|
||||
|
||||
// This is a Contentful client that's been configured
|
||||
// to fetch drafts and unpublished content.
|
||||
const previewClient = createClient({
|
||||
space: CONTENTFUL_SPACE_ID!,
|
||||
accessToken: CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
|
||||
space: NEXT_PUBLIC_CONTENTFUL_SPACE_ID!,
|
||||
accessToken: NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN!,
|
||||
host: 'preview.contentful.com',
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue