diff --git a/apps/strapi/src/api/blog/content-types/blog/schema.json b/apps/strapi/src/api/blog/content-types/blog/schema.json index d6f31d78..55455f75 100644 --- a/apps/strapi/src/api/blog/content-types/blog/schema.json +++ b/apps/strapi/src/api/blog/content-types/blog/schema.json @@ -24,6 +24,11 @@ "type": "component", "component": "forms.newsletter", "repeatable": false + }, + "featuredBlogPost": { + "type": "relation", + "relation": "oneToOne", + "target": "api::blog-post.blog-post" } } } diff --git a/apps/strapi/types/generated/contentTypes.d.ts b/apps/strapi/types/generated/contentTypes.d.ts index fc974d3b..283da2b0 100644 --- a/apps/strapi/types/generated/contentTypes.d.ts +++ b/apps/strapi/types/generated/contentTypes.d.ts @@ -650,6 +650,10 @@ export interface ApiBlogBlog extends Struct.SingleTypeSchema { createdAt: Schema.Attribute.DateTime createdBy: Schema.Attribute.Relation<"oneToOne", "admin::user"> & Schema.Attribute.Private + featuredBlogPost: Schema.Attribute.Relation< + "oneToOne", + "api::blog-post.blog-post" + > locale: Schema.Attribute.String localizations: Schema.Attribute.Relation<"oneToMany", "api::blog.blog"> navigation: Schema.Attribute.Component<"blog.navigation", false> diff --git a/apps/ui/src/app/[locale]/blog/page.tsx b/apps/ui/src/app/[locale]/blog/page.tsx index 531b3fa3..65d68d48 100644 --- a/apps/ui/src/app/[locale]/blog/page.tsx +++ b/apps/ui/src/app/[locale]/blog/page.tsx @@ -14,8 +14,8 @@ import { NewsletterSignup } from "@/components/newsletter/NewsletterSignup" import { StrapiSeoStructuredDataByFullPath } from "@/components/page-builder/components/seo-utilities/StrapiSeoStructuredData" import { BLOG_INDEX_EXCLUDED_CATEGORY_SLUGS, + getBlogIndexPosts, getBlogNewsletterHubspot, - type BlogPost, } from "@/lib/blog-utils" import { getBlogIndexMetadata } from "@/lib/metadata" import { fetchBlog, fetchBlogPostsPage } from "@/lib/strapi-api/content/server" @@ -49,8 +49,10 @@ export default function BlogIndexPage(props: PageProps<"/[locale]/blog">) { ) const hubspotForm = getBlogNewsletterHubspot(blog) - const featuredPost: BlogPost | null = allPosts.posts[0] ?? null - const remainingPosts: BlogPost[] = allPosts.posts.slice(1) + const { featuredPost, remainingPosts, excludeSlugs } = getBlogIndexPosts( + blog?.data, + allPosts.posts + ) return ( <> @@ -68,6 +70,7 @@ export default function BlogIndexPage(props: PageProps<"/[locale]/blog">) { total={allPosts.total} loadMoreLabel={t("loadMore")} excludeCategorySlugs={BLOG_INDEX_EXCLUDED_CATEGORY_SLUGS} + excludeSlugs={excludeSlugs} /> diff --git a/apps/ui/src/components/blog/BlogPostsList.tsx b/apps/ui/src/components/blog/BlogPostsList.tsx index 584deaa2..7bd341b9 100644 --- a/apps/ui/src/components/blog/BlogPostsList.tsx +++ b/apps/ui/src/components/blog/BlogPostsList.tsx @@ -24,6 +24,7 @@ interface BlogPostsListProps { readonly tagSlug?: string readonly authorSlug?: string readonly excludeCategorySlugs?: readonly string[] + readonly excludeSlugs?: readonly string[] } export function BlogPostsList({ @@ -37,6 +38,7 @@ export function BlogPostsList({ tagSlug, authorSlug, excludeCategorySlugs, + excludeSlugs, }: BlogPostsListProps) { const [posts, setPosts] = useState(initialPosts) const [offset, setOffset] = useState(initialOffset) @@ -53,10 +55,11 @@ export function BlogPostsList({ tagSlug, authorSlug, excludeCategorySlugs, + excludeSlugs, }) setPosts((prev) => [...prev, ...res.posts]) - setOffset((prev) => prev + res.posts.length) + setOffset((prev) => prev + res.fetchedCount) setHasMore(res.hasMore) }) } diff --git a/apps/ui/src/components/blog/blog-posts-load-more.ts b/apps/ui/src/components/blog/blog-posts-load-more.ts index 68aa7f6b..5532e696 100644 --- a/apps/ui/src/components/blog/blog-posts-load-more.ts +++ b/apps/ui/src/components/blog/blog-posts-load-more.ts @@ -13,10 +13,12 @@ export interface LoadMoreBlogPostsArgs { readonly tagSlug?: string readonly authorSlug?: string readonly excludeCategorySlugs?: readonly string[] + readonly excludeSlugs?: readonly string[] } export interface LoadMoreBlogPostsResult { readonly posts: BlogPost[] + readonly fetchedCount: number readonly hasMore: boolean } @@ -28,6 +30,7 @@ export async function loadMoreBlogPosts({ tagSlug, authorSlug, excludeCategorySlugs, + excludeSlugs, }: LoadMoreBlogPostsArgs): Promise { const { posts, total } = await fetchBlogPostsPage(locale, { offset, @@ -38,8 +41,15 @@ export async function loadMoreBlogPosts({ excludeCategorySlugs, }) + const excluded = new Set(excludeSlugs) + const filteredPosts = + excluded.size === 0 + ? posts + : posts.filter((post) => !excluded.has(post.slug as string)) + return { - posts, + posts: filteredPosts, + fetchedCount: posts.length, hasMore: offset + posts.length < total, } } diff --git a/apps/ui/src/lib/blog-utils.ts b/apps/ui/src/lib/blog-utils.ts index 61283930..b87dfed8 100644 --- a/apps/ui/src/lib/blog-utils.ts +++ b/apps/ui/src/lib/blog-utils.ts @@ -38,6 +38,33 @@ export function getBlogNewsletterHubspot( return null } +export function getBlogIndexPosts( + blog: BlogData | null | undefined, + posts: readonly BlogPost[] +): { + featuredPost: BlogPost | null + remainingPosts: BlogPost[] + excludeSlugs: string[] | undefined +} { + const hasFeaturedPost = !!blog?.featuredBlogPost + + const featuredPost = hasFeaturedPost + ? (blog?.featuredBlogPost as BlogPost) + : (posts[0] ?? null) + + const remainingPosts = featuredPost + ? posts.filter((post) => post.documentId !== featuredPost.documentId) + : [...posts] + + return { + featuredPost, + remainingPosts, + excludeSlugs: hasFeaturedPost + ? [blog?.featuredBlogPost?.slug as string] + : undefined, + } +} + export function combineAuthors( author: AuthorAvatarData | null | undefined, coauthors?: readonly AuthorAvatarData[] diff --git a/apps/ui/src/lib/strapi-api/content/server.ts b/apps/ui/src/lib/strapi-api/content/server.ts index afd31d44..95a58192 100644 --- a/apps/ui/src/lib/strapi-api/content/server.ts +++ b/apps/ui/src/lib/strapi-api/content/server.ts @@ -750,6 +750,9 @@ export async function fetchBlog(locale: Locale) { newsletter: { populate: { hubspotForm: true }, }, + featuredBlogPost: { + populate: blogListPopulate, + }, }, } satisfies FindFirst<"api::blog.blog">