Loading...
Next.js 14 - header footer contentful tailwindcss - full example - part3
Author Cloudapp
E.G.

Next.js 14 - Complete Example - Header / Footer / Tailwindcss / Contentful - Part 3

March 18, 2024
Table of Contents

In Part 1 we created the foundation of our Next.js 14 project. In Part 2 we created the frontend with Contentful as headless CMS, Tailwindcss for styling, and Typescript for the coding. Now we have reached part 3 and we will add a header and a footer component, as well as the so-called draft/preview functionality offered by Contentful. Not to forget the nowadays mandatory dark mode toggle. Here is already a spoiler to the final result -> Create Cloudapp.dev Example App

New Content Types - Graphql Files

We start with the creation of our new Content Types: Nav Item NavitemGroup FooterItemGroup

Contentful Contenttypes
Contentful Contenttypes

Since we have new Content-Types we must create the corresponding graphql-files under src/lib/graphql

New Graphql Files Contentful Part3
New Graphql Files Contentful Part3

With the new graphql-files in place we can launch the sync command

npm run graphql-codegen:generate

Which updates three files in the folder src/lib/__generated

sdk.ts graphql.schema.json graphql.schema.graphql

Let’s take a look at navItemFields.graphql. As you can see in the screenshot below, we have two fields in the Content-Type “Nav Item” and these two fields are represented in the graphql file.

fragment NavItemFields on NavItem {
  name
  href
}

We reuse this fragment in the other files (navitemgroup.graphql)

fragment NavItemGroupFields on NavItemGroup {
  __typename
  sys {
    id
    spaceId
  }
  name
  navItemsCollection {
    __typename
    items {
      ...NavItemFields
    }
  }
}
query navItemGroup($locale: String, $preview: Boolean) {
  navItemGroupCollection(
    limit: 1
    locale: $locale
    preview: $preview
    where: { mainNav: "Yes" }
  ) {
    items {
      ...NavItemGroupFields
    }
  }
}

Videotutorial creation FooterItemGroup Content Type

New NPM Packages

Now we are ready for the installation of three new NPM packages

npm install @headlessui/react @heroicons/react next-themes

We can now add the new components

New Components Part3
New Components Part3
import Navbar from "@/components/header/navbar.component";
export default function Header({ menuItems }: any) {
  return (
    <header>
      {/* <DarkModeButton /> */}
      {/* <NavbarBanner /> */}
      <Navbar menuItems={menuItems} />
    </header>
  );
}

In the header.component.tsx we receive the “menuItems” as props from layout.tsx and then we send them to the component “Navbar”.

Deeper Look in the Code

Let’s do a short deep dive into layout.tsx (src/app/layout.tsx)

import type { Metadata } from "next";
import { Urbanist } from "next/font/google";
import { draftMode } from "next/headers";
import "./globals.css";
import Header from "@/components/header/header.component";
import Footer from "@/components/footer/footer.component";
import { Providers } from "@/components/header/providers";
import getAllNavitemsForHome from "@/components/header/navbar.menuitems.component";
import getAllFooteritemsForHome from "@/components/footer/footer.menuitems.component";
import ExitDraftModeLink from "@/components/header/draftmode/ExitDraftModeLink.component";

const urbanist = Urbanist({ subsets: ["latin"], variable: "--font-urbanist" });

export const metadata: Metadata = {
  title: "Create Cloudapp.dev Example App",
  description: "Generated by create next app",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const headerdata = await getAllNavitemsForHome();
  const footerdata = await getAllFooteritemsForHome();

  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <main className={`${urbanist.variable} font-sans dark:bg-gray-900`}>
          <Providers>
            <Header menuItems={headerdata} />
            {draftMode().isEnabled && (
              <p className="bg-emerald-400 py-4 px-[6vw]">
                Draft mode is on! <ExitDraftModeLink className="underline" />
              </p>
            )}
            {children}
            <Footer footerItems={footerdata} />
          </Providers>
        </main>
      </body>
    </html>
  );
}  

On the top, we import these components for the new parts

import Header from "@/components/header/header.component";
import Footer from "@/components/footer/footer.component";
import { Providers } from "@/components/header/providers";
import getAllNavitemsForHome from "@/components/header/navbar.menuitems.component";
import getAllFooteritemsForHome from "@/components/footer/footer.menuitems.component";

and the “Providers” component for the dark mode handling.

"use client";
import { ThemeProvider } from "next-themes";
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider enableSystem={true} attribute="class">
      {children}
    </ThemeProvider>
  );
}

Below I point out the return part of the src/app/layout.tsx, where we can see, that we wrapped the header, footer, and {children} into the <Providers></Providers> component, which allows us to use the dark mode toggle on every page.

return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <main className={`${urbanist.variable} font-sans dark:bg-gray-900`}>
          <Providers>
            <Header menuItems={headerdata} />
            {children}
            <Footer footerItems={footerdata} />
          </Providers>
        </main>
      </body>
    </html>
  );

Under {Providers} we import the data for Header/Footer.

import getAllNavitemsForHome from "@/components/header/navbar.menuitems.component";
import getAllFooteritemsForHome from "@/components/footer/footer.menuitems.component";

getAllNavitemsForHome in Detail

import { client } from "@/lib/client";
import { draftMode } from "next/headers";
async function getMenuItems() {
  const { isEnabled } = draftMode();
  const entries = await client.navItemGroup({
    preview: isEnabled,
  });
  return extractNavItemEntries(entries) || [];
}
function extractNavItemEntries(fetchResponse: any) {
  return fetchResponse?.navItemGroupCollection?.items?.[0].navItemsCollection
    ?.items;
}
export default async function getAllNavitemsForHome() {
  const navitems = await getMenuItems();
  return navitems || [];
}

which fetches the data via graphql query from Contentful. client.navItemGroup uses the graphql query specified in src/lib/graphql/navitemgroup.graphql

fragment NavItemGroupFields on NavItemGroup {
  __typename
  sys {
    id
    spaceId
  }
  name
  navItemsCollection {
    __typename
    items {
      ...NavItemFields
    }
  }
}
query navItemGroup($locale: String, $preview: Boolean) {
  navItemGroupCollection(
    limit: 1
    locale: $locale
    preview: $preview
    where: { mainNav: "Yes" }
  ) {
    items {
      ...NavItemGroupFields
    }
  }
}

Because we reuse the same nav items in the header and the footer we defined them only once in graphql.

List of all changed & added files

Here is a complete list of all changed/added files in Part 3 of the tutorial. All files in the folder src/lib/__generated are generated by the command “npm run graphql-codegen:generate” mentioned at the beginning.

Changed Added Files Part3
Changed Added Files Part3

Changes at tailwind.config.ts

The tailwind.config.ts had a small modification as well. We added line 4 and line 27

# Line 4
const extraColors = require("tailwindcss/colors"); //for extra colors e.g. in the Arctile TOC -> emerald
# Line 27
colors: { emerald: extraColors.emerald },
import type { Config } from "tailwindcss";
import tokens from "@contentful/f36-tokens";
const { fontFamily } = require("tailwindcss/defaultTheme");
const extraColors = require("tailwindcss/colors"); //for extra colors e.g. in the Arctile TOC -> emerald

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: {
      colors: { emerald: extraColors.emerald },
      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;

With this change, we added the extra color “emerald”, which we used on line 34 (layout.tsx), within our draft mode banner

Enabling Contentful Draft Mode - Preview

{draftMode().isEnabled && (
   <p className="bg-emerald-400 py-4 px-[6vw]">
     Draft mode is on! <ExitDraftModeLink className="underline" />
   </p>
 )}
DraftMode Banner Header
DraftMode Banner Header

To enable draft/preview mode we modified these pages and components

Draftmode Files
Draftmode Files

and we added these two routes

DraftMode Apis
DraftMode Apis

I added this import

import { draftMode } from "next/headers";

and this constant and I reused the const for the graphql-query (example shows a code piece of src/app/page.tsx

 
sync function BlogPostPage() {
  const { isEnabled } = draftMode();

  const [blogPagedata] = await Promise.all([
    client.pageBlogPost({
      slug: "testblogpost",
      preview: isEnabled,
    }),
  ]);

above I used “client.pageBlogPost” with two params “slug” and “preview”. Slug for filtering (slug is defined as a unique field in Contentful), while preview is used to get the data for content records in “draft” status, which we need for the preview.

query pageBlogPost($slug: String!, $locale: String, $preview: Boolean) {
  pageBlogPostCollection(
    limit: 1
    where: { slug: $slug }
    locale: $locale
    preview: $preview
  ) {
    items {
      ...PageBlogPostFields
    }
  }
}

Configuration Content Preview Contentful

As a last step, we set up the “Content Preview” within Contentful.

Content Preview Menu
Content Preview Menu
Content Preview Menu 2
Content Preview Menu 2

We have to define three fields:

  1. Name -> I used “Preview”

  2. Select content types -> I selected “page — Blog post”

  3. Url -> https://nextjs14-full-example-header-footer.vercel.app/api/draft?previewSecret=xxxxxxxxxxxxxx&redirect=/{entry.fields.slug} (use the value from the env variable -> CONTENTFUL_PREVIEW_SECRET)

Content Preview Menu 3
Content Preview Menu 3

Now let’s open “TestBlogPost3” in Contentful and click on the button “Open Live Preview” on the right side.

Content Preview Menu 4
Content Preview Menu 4

And here is the preview (to test it, modify the Title field on the left side (1) and then click on refresh (2) and you will see the draft mode banner and the change (3))

Content Preview Menu 5
Content Preview Menu 5

GitHub Repo

Here is the full code in the corresponding GitHub branch -> GitHub - cloudapp-dev/nextjs14-typescript-app-router-contentful at nextjs14-part3

where you also find the updated export.json file (folder “contentfulsync”), which you can use to import the data to contentful

npm run cf-import

Stay tuned for the next posts where we will add new functions like an Azure Entra ID (formerly Azure AD B2C) for identity management Algolia as an On-Site Search Engine and a lot more.

If you like what you see, then please support me with a clap or follow me on medium.com.

Related articles