Loading...
Next.js 14 - typescript contentful tailwindcss - full example
Author Cloudapp
E.G.

Next.js 14 - Komplettes Beispiel - Typescript / Tailwindcss / Contentful - Teil 2

11. März 2024
Inhaltsverzeichnis

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

Contentful seoFields Content Type
Contentful seoFields Content Type

Unten eine Übersicht aller Inhaltstypen aus Contentful

Contentful overview content types
Contentful overview content types

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@latest

f36-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 config

Anpassungen 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.

Durch 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.

Hier ist eine gute Erklärung zu Basis-Layern von tailwindcss.com https://v1.tailwindcss.com/docs/adding-base-styles


@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;
  }
}
Die Datei global.css wird oben in der Datei layout.tsx eingebunden


##src/app/layout.tsx##
import './globals.css'
Wir sind jetzt bereit für die neuen Komponenten, die wir hinzufügen müssen, und die Änderungen an der page.tsx. Insgesamt werden wir 6 Komponenten hinzufügen:
Vscode new components and changed page.tsx
Vscode new components and changed page.tsx
In der Datei src/app/page.tsx haben wir folgende Codezeilen hinzugefügt Neu Importe
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>
}
Dynamic Segments
Dynamic Segments

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“.

Testblog
Testblog

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.

Verwandte Artikel