Wenn Ihr Blog wächst, brauchen Sie eine Funktion wie "Mehr laden" oder sogar "Unendliches Scrollen", damit Ihre Besucher leicht auf alle wertvollen Inhalte zugreifen können. In diesem Beitrag werde ich mich darauf konzentrieren und Ihnen zeigen, wie einfach es ist, eine solche Funktion mit Next.js 14, Server Actions (um die schwere Arbeit auf den Server zu verlagern) und Contentful als zugrunde liegendes CMS, in dem wir alle unsere Daten haben, zu implementieren.
Hier ist das GitHub repo mit dem vollständigen Code.
Hier der Link zur Testseite, sodass Sie sich bereits einen Überblick verschaffen können.
https://nextjs14-azureb2c-prisma.vercel.app/
Neues NPM Paket
Lassen Sie uns das neue NPM-Paket "react-intersection-observer" installieren, welches wir für das "Endless Scroll" benötigen.
npm i react-intersection-observerNext.js 14 Server Action - Fürs Abrufen der Posts aus Contentful
Nun erstelle ich einen neue Datei im Verzeichnis “src/actions” mit dem Namen “getPosts.ts”
"use server";
import type { UserAPIResponse } from "@/types/userScroll";
import { PageBlogPostOrder } from "@/lib/__generated/sdk";
import { client } from "@/lib/client";
export const getPosts = async (
offset: number,
limit: number,
locale: string,
isEnabled: boolean,
slug_not: string
) => {
try {
// Getting BlogPosts
const blogPostsData = await client.pageBlogPostCollection({
limit: limit,
locale: locale,
skip: offset,
preview: isEnabled,
order: PageBlogPostOrder.PublishedDateDesc,
where: {
slug_not: slug_not,
},
});
const posts: any = blogPostsData.pageBlogPostCollection?.items;
return posts;
} catch (error: unknown) {
console.log(error);
throw new Error(`An error happened: ${error}`);
}
};Wie Sie sehen können, verwenden wir die Direktive „use server“, die Next.js anweist, sie nur auf dem Server auszuführen.
Server Actions kurz erklärt
Server Actions sind asynchrone Funktionen, die auf dem Server ausgeführt werden. Sie können in Server- und Client-Komponenten verwendet werden, um Formularübermittlungen und Datenmutationen in Next.js-Anwendungen zu handhaben.
Da diese speziellen Funktionen nur auf dem Server ausgeführt werden, können Entwickler Verantwortlichkeiten wie das Abrufen und Mutieren von Daten an diese auslagern, wodurch die Schwachstellen und Sicherheitsbedenken vermieden werden, die beim Abrufen und Mutieren von Daten vom Client aus auftreten.
Contentful GraphQL- Nutzung eines Offsets mit Hilfe des Skip Parameters
Als Nächstes passe ich die zugrunde liegende GraphQL-Datei für das Abrufen der „posts“-Daten von Contentful an. Ich habe die Variable „skip“ hinzugefügt, damit wir beim Datenabruf mit einem Offset arbeiten können.
query pageBlogPostCollection(
$locale: String
$preview: Boolean
$limit: Int
$skip: Int
$order: [PageBlogPostOrder]
$where: PageBlogPostFilter
) {
pageBlogPostCollection(
limit: $limit
skip: $skip
locale: $locale
preview: $preview
order: $order
where: $where
) {
items {
...PageBlogPostFields
}
}
}Anpassen der Logik der page.tsx Datei
Die nächste Datei, die angepasst werden muss, ist die page.tsx unter „src/app/[locale]“
import { client } from "@/lib/client";
import { notFound } from "next/navigation";
import { ArticleHero } from "@/components/contentful/ArticleHero";
import { ArticleTileGrid } from "@/components/contentful/ArticleTileGrid";
import { Container } from "@/components/contentful/container/Container";
import { draftMode } from "next/headers";
// Internationalization
import { locales, LocaleTypes } from "@/app/i18n/settings";
import { createTranslation } from "@/app/i18n/server";
//SEO - JSON-LD
import { Article, WithContext } from "schema-dts";
import path from "path";
import Script from "next/script";
import { Metadata, ResolvingMetadata } from "next";
// New Fields Part 9 of tutorial
import { PageBlogPostOrder } from "@/lib/__generated/sdk";
import { TextHighLight } from "@/components/contentful/TextHighLight";
import { revalidateDuration } from "@/utils/constants";
import { TagCloudSimpleHome } from "@/components/search/tagcloudsimpleHome.component";
import Link from "next/link";
import { LandingContent } from "@/components/contentful/ArticleContentLanding";
export const revalidate = revalidateDuration; // revalidate at most every hour
interface PageParams {
slug: string;
locale: string;
}
interface PageProps {
params: PageParams;
}
const generateUrl = (locale: string, slug: string) => {
if (locale === "en-US") {
return new URL(slug, process.env.NEXT_PUBLIC_BASE_URL!).toString();
} else {
return new URL(locale, process.env.NEXT_PUBLIC_BASE_URL!).toString();
}
};
const WebUrl = process.env.NEXT_PUBLIC_BASE_URL as string;
export async function generateMetadata(
{ params }: PageProps,
parent: ResolvingMetadata
): Promise<Metadata> {
const [PagedataSeo] = await Promise.all([
client.pageLanding({
slug: "/",
locale: params.locale.toString(),
preview: draftMode().isEnabled,
}),
]);
const landingPage = PagedataSeo.pageLandingCollection?.items[0];
if (!landingPage) {
return notFound();
}
const url = generateUrl(params.locale || "", "");
return {
title: landingPage.seoFields?.pageTitle,
description: landingPage.seoFields?.pageDescription,
metadataBase: new URL(WebUrl),
alternates: {
canonical: url,
languages: {
"en-US": "/",
"de-DE": "/de-DE",
"x-default": "/",
},
},
openGraph: {
type: "website",
siteName: "Example.dev - Free Tutorials and Resources for Developers",
locale: params.locale,
url: url || "",
title: landingPage.seoFields?.pageTitle || undefined,
description: landingPage.seoFields?.pageDescription || undefined,
images: landingPage.seoFields?.shareImagesCollection?.items.map(
(item) => ({
url: item?.url || "",
width: item?.width || 0,
height: item?.height || 0,
alt: item?.description || "",
type: item?.contentType || "",
})
),
},
robots: {
follow: landingPage.seoFields?.follow || false,
index: landingPage.seoFields?.index || false,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
},
},
};
}
async function Home({ params }: PageProps) {
const { isEnabled } = draftMode();
//declare JSON-LD schema
let jsonLd: WithContext<Article> = {} as WithContext<Article>;
const [landingPageData] = await Promise.all([
client.pageLanding({
slug: "/",
locale: params.locale.toString(),
preview: isEnabled,
}),
]);
const page = landingPageData.pageLandingCollection?.items[0];
if (!page) {
// If a blog post can't be found,
// tell Next.js to render a 404 page.
return notFound();
}
// TagCloud
const showTagCloud = page.showTagCloud === "Yes";
let { datanew, minSize, maxSize } = {
datanew: [],
minSize: 0,
maxSize: 0,
};
if (showTagCloud) {
const searchFacets = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/search/facets`,
{}
)
.then((res) => res.json())
.catch((error) => {
console.log("No data found");
});
if (searchFacets) {
maxSize = searchFacets.maxSize;
minSize = searchFacets.minSize;
datanew = searchFacets.datanew;
}
}
// Getting BlogPosts
const blogPostsData = await client.pageBlogPostCollection({
limit: 10,
locale: params.locale.toString(),
skip: 0,
preview: isEnabled,
order: PageBlogPostOrder.PublishedDateDesc,
where: {
slug_not: page?.featuredBlogPost?.slug,
},
});
const posts = blogPostsData.pageBlogPostCollection?.items;
// Create JSON-LD schema only if blogPost is available
if (page) {
jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: page?.seoFields?.pageTitle || undefined,
author: {
"@type": "Person",
name: page?.featuredBlogPost?.author?.name || undefined,
// The full URL must be provided, including the website's domain.
url: new URL(
path.join(
params.locale.toString() || "",
params.slug?.toString() || ""
),
process.env.NEXT_PUBLIC_BASE_URL!
).toString(),
},
publisher: {
"@type": "Organization",
name: "Example.dev - Free Tutorials and Resources for Developers",
logo: {
"@type": "ImageObject",
url: "https://www.example.dev/favicons/icon-192x192.png",
},
},
image: page?.featuredBlogPost?.featuredImage?.url || undefined,
datePublished: page.sys.firstPublishedAt,
dateModified: page.sys.publishedAt,
};
}
// Internationalization, get the translation function
const { t } = await createTranslation(params.locale as LocaleTypes, "common");
const highLightHeadings: any = page.textHighlightCollection?.items[0];
if (!page?.featuredBlogPost || !posts) return;
return (
<>
{page && (
<Script
id="article-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd),
}}
/>
)}
<Container className="mt-5">
{highLightHeadings && <TextHighLight headings={highLightHeadings} />}
{showTagCloud && datanew.length > 0 && (
<TagCloudSimpleHome
datanew={datanew}
minSize={minSize * 10}
maxSize={maxSize * 5}
locale={params.locale.toString()}
source={"homepage"}
/>
)}
<Link
href={`/${params.locale.toString()}/${page.featuredBlogPost.slug}`}
>
<ArticleHero article={page.featuredBlogPost} isHomePage={true} />
</Link>
<div className="md:mx-24 md:my-24 sm:mx-16 sm:my-16">
<LandingContent landing={page} />
</div>
</Container>
<Container className="my-8 md:mb-10 lg:mb-16">
{posts.length > 0 && (
<h2 className="mb-4 md:mb-6">{t("landingPage.latestArticles")}</h2>
)}
<ArticleTileGrid
className="grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
articles={posts}
slug={page.featuredBlogPost.slug}
locale={params.locale.toString()}
/>
</Container>
</>
);
}
export default Home;In Zeile 154 führen wir den Parameter „skip“ ein, und im letzten Abschnitt der Datei müssen wir zwei neue Parameter (slug und locale) an unsere „ArticleTileGrid“-Komponente übergeben.
<ArticleTileGrid
className="grid-cols-1 md:grid-cols-2 lg:grid-cols-4"
articles={posts}
slug={page.featuredBlogPost.slug}
locale={params.locale.toString()}
/>Der Slug Parameter wird benötigt, um die BlogPosts im Hero-Abschnitt der Seite auszuschließen, damit wir sie nicht doppelt anzeigen, und ein Locale wird benötigt, um den mehrsprachigen Teil zu handhaben.
ArticleTileGrid - Wo die Logik beheimatet ist
Zu guter Letzt passen wir die Komponente „ArticleTileGrid“ an, wo das meiste der Magie passiert..
"use client";
import { HTMLProps } from "react";
import { twMerge } from "tailwind-merge";
import { Button } from "@tremor/react";
import { ArticleTile } from "@/components/contentful/ArticleTile";
import { PageBlogPostFieldsFragment } from "@/lib/__generated/sdk";
import { useInView } from "react-intersection-observer";
import { useState, useEffect } from "react";
import { getPosts } from "@/actions/getPosts";
interface ArticleTileGridProps extends HTMLProps<HTMLDivElement> {
articles?: Array<PageBlogPostFieldsFragment | null>;
slug: string | null | undefined;
locale: string;
}
const NUMBER_OF_USERS_TO_FETCH = 10;
export const ArticleTileGrid = ({
articles,
className,
slug,
locale,
...props
}: ArticleTileGridProps) => {
const [offset, setOffset] = useState(NUMBER_OF_USERS_TO_FETCH);
const [posts, setPosts] = useState<any>(articles);
const { ref, inView } = useInView();
const loadMoreUsers = async () => {
const apiPosts = await getPosts(
offset,
NUMBER_OF_USERS_TO_FETCH,
locale || "",
false,
slug || ""
);
setPosts([...posts, ...apiPosts]);
setOffset(offset + NUMBER_OF_USERS_TO_FETCH);
};
//Infinte scroll
// useEffect(() => {
// if (inView) {
// loadMoreUsers();
// }
// }, [inView]);
return posts && posts.length > 0 ? (
<>
<div
className={twMerge(
"grid grid-cols-1 gap-y-4 gap-x-5 md:grid-cols-4 lg:gap-x-12 lg:gap-y-12",
className
)}
{...props}
>
{posts.map((article: any, index: number) => {
return article ? <ArticleTile key={index} article={article} /> : null;
})}
</div>
{/* Infinite Scroll */}
{/* <div ref={ref}>Loading...</div> */}
<div className="flex flex-col items-center">
<Button
onClick={loadMoreUsers}
size="lg"
className="mt-4 sm:flex sm:max-w-md"
>
Load more
</Button>
</div>
</>
) : null;
};In dieser Komponente können Sie entscheiden, ob Sie die Schaltfläche „Mehr laden“ oder die Methode „Unendliches Scrollen“ verwenden möchten.

Cloudapp-dev und bevor Sie uns verlassen
Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:



