Lassen Sie uns weiter machen auf Basis des vorherigen Beitrags Nextjs 14 - Complete Example with Typescript - Contentful - App Router
Check Contentful Inhaltstypen
Unten sehen Sie die Graphql Abfrage für den Inhaltstyp SeoFields aus der Datei seoFields.graphql (Ordner -> src/lib/graphql)
fragment SeoFields on ComponentSeo {
__typename
pageTitle
pageDescription
canonicalUrl
follow
index
shareImagesCollection(limit: 3, locale: $locale) {
items {
...ImageFields
}
}
}dieser Screenshot zeigt die verschiedenen Datenfelder des seoFields Inhaltstyps im Contentful Backend

Unten eine Übersicht aller Inhaltstypen aus Contentful

Nun verbessern wird das Frontend des Blogs.
Installation der benötigten NPM Pakete
Zuerst aktualisieren bzw. installieren wir die folgenden Npm Pakete
npm install @contentful/f36-tokens @tailwindcss/typography tailwindcss@latestf36-tokens ist ein Teil des F36-Designsystems von Contentful. Jetzt modifizieren wir die Hauptkonfigurationsdatei von Tailwindcss: tailwind.config.ts im Wurzelverzeichnis des Projekts.
Vorher
##old tailwind.config.ts##
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [],
}
export default configAnpassungen an der Datei tailwind.config.ts
Danach
##new tailwind.config.ts##
import type { Config } from "tailwindcss";
import tokens from "@contentful/f36-tokens";
const { fontFamily } = require("tailwindcss/defaultTheme");
const colors = Object.entries(tokens).reduce(
(acc: Record<string, any>, [key, value]) => {
// Filter Hex colors from the f36-tokens
if (/^#[0-9A-F]{6}$/i.test(value as any)) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);
const config: Config = {
darkMode: "class",
content: [
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
colors,
extend: {
maxWidth: {
"8xl": "90rem",
},
letterSpacing: {
snug: "-0.011em",
},
boxShadow: {
// light
"tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
"tremor-card":
"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
"tremor-dropdown":
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
},
borderRadius: {
"tremor-small": "0.375rem",
"tremor-default": "0.5rem",
"tremor-full": "9999px",
},
fontSize: {
"2xs": "0.625rem",
"3xl": "1.75rem",
"4xl": "2.5rem",
"tremor-label": "0.75rem",
"tremor-default": ["1.0rem", { lineHeight: "1.25rem" }],
"tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
"tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
},
lineHeight: {
tighter: "1.1",
},
fontFamily: {
sans: ["var(--font-urbanist)", ...fontFamily.sans],
},
},
},
plugins: [require("@tailwindcss/typography")],
};
export default config;Zuerst importieren wir
import tokens from "@contentful/f36-tokens";
const { fontFamily } = require("tailwindcss/defaultTheme");dann durchlaufen wir die Tokens (filtern dabei nur die Hex-Farben) und verwenden die neuen "Farben" in der tailwind.config.ts Datei -> im Abschnitt "theme".
const colors = Object.entries(tokens).reduce((acc: Record<string, any>, [key, value]) => {
// Filter Hex colors from the f36-tokens
if (/^#[0-9A-F]{6}$/i.test(value as any)) {
acc[key] = value;
}
return acc;
}, {} as Record<string, string>);Anpassungen an der Datei global.css
Als nächsten Schritt passen wir die Datei global.css unter src/app an. Wir fügen einige Stile als Basis-Layer hinzu.
Hier ist eine gute Erklärung zu Basis-Layern von tailwindcss.com https://v1.tailwindcss.com/docs/adding-base-stylesDurch die Verwendung der @layer-Direktive wird Tailwind diese Stile automatisch an denselben Ort wie @tailwind base verschieben, um unbeabsichtigte Spezifitätsprobleme zu vermeiden. Die Verwendung der @layer-Direktive weist Tailwind außerdem an, diese Stile beim Bereinigen der Basis-Schicht zu berücksichtigen. Wir verwenden @apply, um diese Stile zu definieren, um das Einführen neuer magischer Werte zu vermeiden oder versehentlich von Ihrem Designsystem abzuweichen.
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply text-sm dark:text-gray400 text-gray800 md:text-sm;
}
.h1,
h1 {
@apply text-xl font-semibold dark:text-[#FAFAFA] text-gray600 leading-tighter md:text-4xl;
}
.h2,
h2 {
@apply text-xl font-semibold leading-tight dark:text-[#FAFAFA] text-gray600 md:text-3xl;
}
.h3,
h3 {
@apply text-base font-semibold leading-relaxed dark:text-[#FAFAFA] text-gray600 md:text-xl;
}
.h4,
h4 {
@apply text-xs font-semibold leading-normal dark:text-[#FAFAFA] text-gray600 md:text-base;
}
p {
@apply text-xl leading-normal dark:text-[#FAFAFA] text-gray600 tracking-snug md:leading-normal;
}
}
##src/app/layout.tsx##
import './globals.css'
import { ArticleHero } from "@/components/contentful/ArticleHero";
import { ArticleTileGrid } from "@/components/contentful/ArticleTileGrid";
import { Container } from "@/components/contentful/container/Container";Nachdem wir die neuen Importe hinzugefügt haben, sollten folgende Zeilen vorhanden sein
import { ArticleContent } from "@/components/contentful/ArticleContent.component";
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";Lassen Sie uns einen neuen Ordner [slug] unter src/app hinzufügen. Wenn ein Ordnername in eckige Klammern gesetzt ist, erstellen wir ein „Dynamisches Segment“.
Beispiel
Zum Beispiel könnte ein Blog die folgende Route app/blog/[slug]/page.tsx beinhalten, wobei [slug] das dynamische Segment für Blogbeiträge ist.
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>
}
Im neuen Ordner [slug] erstellen wir eine neue Datei page.tsx, in der wir auch unsere neuen Komponenten verwenden.
import { ArticleContent } from "@/components/contentful/ArticleContent.component";
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";
interface BlogPostPageParams {
slug: string;
locale: string;
}
interface BlogPostPageProps {
params: BlogPostPageParams;
}
const locales = ["de-DE"]; //Fake locales for the purpose of the example
// Tell Next.js about all our blog posts so
// they can be statically generated at build time.
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
const dataPerLocale = locales
? await Promise.all(
locales.map((locale) => client.pageBlogPostCollection({ limit: 100 }))
)
: [];
const paths = dataPerLocale
.flatMap((data, index) =>
data.pageBlogPostCollection?.items.map((blogPost) =>
blogPost?.slug
? {
slug: blogPost.slug,
locale: locales?.[index] || "",
}
: undefined
)
)
.filter(Boolean);
return paths as BlogPostPageParams[];
}
async function BlogPostPage({ params }: BlogPostPageProps) {
const [blogPagedata] = await Promise.all([
client.pageBlogPost({
slug: params.slug.toString(),
}),
]);
const blogPost = blogPagedata.pageBlogPostCollection?.items[0];
if (!blogPost) {
// If a blog post can't be found,
// tell Next.js to render a 404 page.
return notFound();
}
const relatedPosts = blogPost?.relatedBlogPostsCollection?.items;
if (!blogPost || !relatedPosts) return null;
return (
<>
<div className="mt-4" />
<Container>
<ArticleHero
article={blogPost}
isReversedLayout={true}
isHomePage={false}
/>
</Container>
<Container className="max-w-4xl mt-8">
<ArticleContent article={blogPost} />
</Container>
{relatedPosts.length > 0 && (
<Container className="max-w-5xl mt-8">
<h2 className="mb-4 md:mb-6">Related Posts</h2>
<ArticleTileGrid className="md:grid-cols-2" articles={relatedPosts} />
</Container>
)}
</>
);
}
export default BlogPostPage;unter den Importen erstellen wir unsere "Interfaces"
Interface ist eine Struktur, die den Vertrag in Ihrer Anwendung definiert. Sie legt die Syntax für Klassen fest, denen sie folgen müssen. Klassen, die von einem Interface abgeleitet sind, müssen die von ihrem Interface vorgegebene Struktur befolgen. Ein Interface wird mit dem Schlüsselwort interface definiert, und es kann Eigenschaften und Methodendeklarationen enthalten, die mit einer Funktion oder einer Pfeilfunktion verwendet werden.
interface BlogPostPageParams {
slug: string;
locale: string;
}
interface BlogPostPageProps {
params: BlogPostPageParams;
}Die Funktion generateStaticParams kann in Kombination mit dynamischen Routensegmenten verwendet werden, um Routen zur Build-Zeit statisch zu generieren, anstatt sie auf Anfrage zur Laufzeit zu generieren.
Der primäre Vorteil der Funktion generateStaticParams liegt in ihrer intelligenten Datenerfassung. Wenn Inhalte innerhalb der Funktion generateStaticParams über eine Fetch-Anfrage abgerufen werden, werden die Anfragen automatisch gespeichert. Das bedeutet, dass eine Fetch-Anfrage mit denselben Argumenten über mehrere generateStaticParams, Layouts und Seiten hinweg nur einmal ausgeführt wird, was die Build-Zeiten verkürzt.
export async function generateStaticParams(): Promise<BlogPostPageParams[]> {
const dataPerLocale = locales
? await Promise.all(
locales.map((locale) => client.pageBlogPostCollection({ limit: 100 }))
)
: [];
const paths = dataPerLocale
.flatMap((data, index) =>
data.pageBlogPostCollection?.items.map((blogPost) =>
blogPost?.slug
? {
slug: blogPost.slug,
locale: locales?.[index] || "",
}
: undefined
)
)
.filter(Boolean);
return paths as BlogPostPageParams[];
}Unten ist die neue Version unseres Blogs. Starten Sie den Entwicklungs-Server mit „npm run dev“ und öffnen Sie „http://localhost:3000“.

Jetzt funktioniert unser Blog, wir haben eine Homepage, und wenn wir unten auf die Blogbeiträge klicken, gelangen wir auf die entsprechende Blogbeitragsseite.
GitHub Repo
Bleiben Sie dran für die nächsten Beiträge, in denen wir neue Komponenten wie Navbar, Footer usw. hinzufügen werden. Außerdem werden wir eine Azure Entra-ID (früher bekannt als Azure AD B2C) für das Identitätsmanagement und Algolia als On-Site-Suchmaschine hinzufügen.
Hier gehts zum Code in GitHub Repo
Wenn Ihnen gefällt, was Sie sehen, dann unterstützen Sie mich bitte mit einem "Clap" oder folgen Sie mir auf medium.com.




