Home / Companies / Strapi / Blog / March 2024

March 2024 Summaries

13 posts from Strapi

Filter
Month: Year:
Post Summaries Back to Blog
This guide explores the use of API tokens and JSON Web Tokens (JWTs) in web security, specifically focusing on their implementation in Strapi for building secure web applications. It covers the generation, management, and implementation of these mechanisms, as well as how to create a collection type using the Content-Type Builder. The article also discusses how to manage API tokens and JWT tokens in Strapi's admin panel, make authenticated requests with Postman, and apply security practices such as HTTPS and SSL for secure token storage.
Mar 27, 2024 1,563 words in the original blog post.
In this tutorial, we will continue working on our Next.js application by adding a Dashboard page that displays user information and provides a Logout button. We will also add some basic styling to make the dashboard more visually appealing. Let's get started! First, let's create a new folder called `dashboard` inside of our `app/pages` directory. Inside this new folder, create a file named `page.tsx`. This is where we will build out our Dashboard page. Next, open up the `auth-actions.ts` file and add the following code to the end of the file: ```typescript export async function logoutAction() { cookies().set("jwt", "", { ...config, maxAge: 0 }); redirect("/"); } ``` This new `logoutAction` function will be used to handle the user's logout action. It sets the JWT cookie value to an empty string and then redirects the user back to the home page. Now, let's update our `SigninForm.tsx` file by adding a new import statement at the top of the file: ```typescript import { useRouter } from "next/navigation"; // ... rest of the code remains unchanged ``` We will be using this new `useRouter` hook to handle our redirects after logging in or out. Next, update the `redirect("/dashboard")` line inside of both the `registerUserAction` and `loginUserAction` functions by replacing it with the following code: ```typescript const router = useRouter(); router.push("/dashboard"); ``` This change will now use the `useRouter` hook to handle our redirects instead of using the `redirect` function directly. Now, let's update our `DashboardRoute.tsx` file by adding some basic styling and updating the Logout button functionality: ```typescriptx import { LogoutButton } from "@/components/custom/LogoutButton"; export default function DashboardRoute() { const router = useRouter(); return ( <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 dark:bg-gray-900"> <h1>Dashboard</h1> <div className="space-y-4"> <div className="flex space-x-2"> <span className="font-bold text-lg">Username:</span> <span>{user?.username}</span> </div> <div className="flex space-x-2"> <span className="font-bold text-lg">Email:</span> <span>{user?.email}</span> </div> </div> <LogoutButton /> </div> ); } ``` In this updated code, we have added some basic styling to the Dashboard page and also included a new `useRouter` hook import statement at the top of the file. We are now using this new hook to handle our redirects after logging out. Finally, let's update our `LogoutButton.tsx` file by adding the following code inside of the button element: ```typescriptx <button type="submit" onClick={() => { cookies().set("jwt", "", { ...config, maxAge: 0 }); router.push("/"); }}> <LogOut className="w-6 h-6 hover:text-primary" /> </button> ``` This new code will now handle the user's logout action by setting the JWT cookie value to an empty string and then redirecting the user back to the home page. That's it! We have now successfully built out our Dashboard page with basic styling and added a Logout button that securely logs users out of their accounts. In this Next.js tutorial, we continued working on our application by adding a Dashboard page that displays user information and provides a Logout button. We also covered updating the Sign In and Sign Up pages to use the `useRouter` hook for handling redirects after logging in or out. Thank you for your time, and I hope you are enjoying these tutorials. If you have any questions, you can ask them in the comments or stop by Strapi's open office on Discord from 12:30 pm CST to 1:30 pm CST Monday through Friday. See you in the next post, where we will work on adding user authentication flows to our Next.js application using server actions and httpOnly cookies for secure login functionality.``` SUMMARY:
Mar 26, 2024 5,616 words in the original blog post.
In this tutorial, we will build a Sign Up and Sign In page for a Next.js application using Strapi as our backend API. We will use server actions to handle form submissions and integrate with the backend API. Additionally, we will cover setting up httpOnly cookies for secure authentication and protecting routes through Next.js middleware. First, let's create a new Next.js project: ```bash npx create-next-app@latest --use-npm ``` Next, install the necessary dependencies: ```bash npm install @strapi/react react-router-dom zod zod-form-data axios ``` Now, let's create a new file called `utils.js` in the root directory of our project and add the following code to it: ```javascript // utils.js export const getStrapiURL = () => { return process.env.STRAPI_URL || "http://localhost:1337"; }; export const getAuthToken = async () => { const cookieStore = await cookies(); const authToken = cookieStore.get("jwt")?.value; return authToken; }; ``` Now, let's create a new file called `auth-actions.js` in the root directory of our project and add the following code to it: ```javascript // auth-actions.js import { z } from "zod"; import axios from "axios"; import { getStrapiURL, getAuthToken } from "./utils"; const schemaRegister = z.object({ username: z.string().min(3).max(20), email: z.string().email(), password: z.string().min(6).max(100), }); export async function registerUserAction(prevState, formData) { const validatedFields = schemaRegister.safeParse(formData); if (!validatedFields.success) { return { ...prevState, zodErrors: validatedFields.error.flatten() }; } try { const response = await axios.post(`${getStrapiURL()}/auth/local/register`, { username: validatedFields.data.username, email: validatedFields.data.email, password: validatedFields.data.password, }); if (response.status === 200) { return { ...prevState, message: "User registered successfully" }; } else { throw new Error("Failed to register user"); } } catch (error) { console.error(error); return { ...prevState, strapiErrors: error.message }; } } const schemaLogin = z.object({ identifier: z.string().min(3).max(20), password: z.string().min(6).max(100), }); export async function loginUserAction(prevState, formData) { const validatedFields = schemaLogin.safeParse(formData); if (!validatedFields.success) { return { ...prevState, zodErrors: validatedFields.error.flatten() }; } try { const response = await axios.post(`${getStrapiURL()}/auth/local`, { identifier: validatedFields.data.identifier, password: validatedFields.data.password, }); if (response.status === 200) { const authToken = response.data.jwt; setCookie("jwt", authToken, { ...config, maxAge: 60 * 60 * 24 * 7 }); return { ...prevState, message: "Logged in successfully" }; } else { throw new Error("Failed to log in"); } } catch (error) { console.error(error); return { ...prevState, strapiErrors: error.message }; } } export async function logoutAction() { try { const authToken = await getAuthToken(); if (!authToken) { throw new Error("No token found"); } const response = await axios.post(`${getStrapiURL()}/users/logout`, null, { headers: { Authorization: `Bearer ${authToken}` }, }); if (response.status === 200) { removeCookie("jwt"); return { ...prevState, message: "Logged out successfully" }; } else { throw new Error("Failed to logout"); } } catch (error) { console.error(error); return { ...prevState, strapiErrors: error.message }; } } ``` Now, let's create a new file called `signup-form.js` in the root directory of our project and add the following code to it: ```javascript // signup-form.js import { useActionState } from "react"; import { schemaRegister } from "./auth-actions"; import { Input, Button, FormControl, Text } from "@chakra-ui/react"; import { ZodErrors } from "../components/custom/zod-errors"; const INITIAL_STATE = { zodErrors: null, strapiErrors: null, data: null, message: null, }; export function SignupForm() { const [formState, formAction] = useActionState(registerUserAction, INITIAL_STATE); return ( <div> <Text fontSize="2xl" mb={4}>Sign Up</Text> <form action={formAction}> <FormControl isInvalid={!!formState.zodErrors?.username}> <Input id="username" name="username" type="text" placeholder="Username" value={formState.data?.username || ""} onChange={(e) => formAction({ ...formState, data: { ...formState.data, username: e.target.value } })} /> <ZodErrors error={formState.zodErrors?.username} /> </FormControl> <FormControl isInvalid={!!formState.zodErrors?.email}> <Input id="email" name="email" type="email" placeholder="Email" value={formState.data?.email || ""} onChange={(e) => formAction({ ...formState, data: { ...formState.data, email: e.target.value } })} /> <ZodErrors error={formState.zodErrors?.email} /> </FormControl> <FormControl isInvalid={!!formState.zodErrors?.password}> <Input id="password" name="password" type="password" 78910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112121312141215121612171218121912212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121213121412151216121712181219122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
Mar 26, 2024 6,261 words in the original blog post.
Strapi has released the beta version of its highly anticipated Strapi 5, which promises to be more powerful, flexible, and user-friendly than previous versions. The new release includes a user-friendly interface with separate tabs for draft and published content, quicker build times and better performance due to Vite integration, an updated API format, enhanced GraphQL support, and dual-format compatibility for smooth migration from Strapi 4. The beta version is available for testing and exploration purposes only, and the Strapi team encourages users to provide feedback and bug reports to help improve the final release.
Mar 25, 2024 1,167 words in the original blog post.
This article provides a non-technical approach to using Strapi's content management feature to create a landing page from scratch using dynamic zone components. It demonstrates how to start a new Strapi project, register as an administrator, and navigate the admin dashboard. The author explains how to create a Stri-Fitness collection with fields for title and description, and then introduces components and dynamic zones. They walk through creating image and text components, adding dynamic zones to the website, and populating content in the landing page. Finally, they discuss how developers can consume Strapi endpoints to display the page on the client side.
Mar 20, 2024 1,052 words in the original blog post.
In this tutorial, we continued building a real-world project using Next.js 14 and Strapi CMS. We covered several key areas: 1. Refactoring the Hero Section: We refactored the Hero Section to use the Next.js Image component for optimized image handling. This included creating a custom StrapiImage component for additional quality-of-life improvements. 2. Building the Features Section: This section involved modeling the Features Section data in Strapi, creating corresponding components in Next.js, and implementing functionality to display features dynamically from the Strapi CMS. 3. Displaying Dynamic Meta Data: We examined how to get our metadata from Strapi and display it on our layout.tsx page. 4. Top Header and Footer: We created our Header and Footer, leveraging Strapi to manage and fetch global data like logo texts and social links. 5. Loading, Not Found, and Error Pages: We finished by covering how to handle loading, not found, and errors pages. In the next tutorial, we will cover creating our Sign In and Sign Up pages. This will include form validation with Zod, handling form submission with server actions, creating and storing http only cookies, and protecting our routes with Next.js middleware.
Mar 19, 2024 6,995 words in the original blog post.
In this tutorial, we will continue building our landing page using Next.js and Strapi. We will refactor the Hero Section to use the Next.js Image component for optimized image handling, build the Features Section by modeling data in Strapi and creating corresponding components in Next.js, display dynamic metadata from Strapi on our layout.tsx page, create a top header and footer using global data fetched from Strapi, and handle loading, not found, and error pages. To follow along with this tutorial, you should have completed the previous parts of the Epic Next.js 14 Tutorial series or have an existing project set up with Next.js and Strapi. You can find the complete code for this tutorial on GitHub. Let's get started! ## Refactoring the Hero Section First, let's refactor the Hero Section to use the Next.js Image component for optimized image handling. This will improve our website's performance by lazy-loading images and serving them in modern formats like WebP. 1. Create a custom StrapiImage component inside the `components` folder: ```tsx // components/StrapiImage.tsx import Image from "next/image"; interface Props { image: any; } export default function StrapiImage({ image }: Props) { return ( <Image src={image?.data?.attributes?.url || "/"} alt={image?.data?.attributes?.alternativeText || ""} width={image?.data?.attributes?.width || 0} height={image?.data?.attributes?.height || 0} layout="fill" objectFit="cover" /> ); } ``` 2. Update the `HeroSection` component to use the custom StrapiImage component: ```tsx // components/HeroSection.tsx import { Block } from "@/lib/types"; import Image from "next/image"; import Link from "next/link"; import { cn } from "@/lib/utils"; import StrapiImage from "./StrapiImage"; interface Props { block: Block; } export default function HeroSection({ block }: Props) { return ( <section className="bg-gray-100 dark:bg-gray-900"> <div className="max-w-screen-2xl px-4 py-16 mx-auto sm:px-6 lg:px-8"> {block?.image && ( <StrapiImage image={block.image} /> )} <div className="max-w-lg mx-auto text-center"> <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 sm:text-4xl"> {block?.title || ""} </h1> <p className="max-w-md mx-auto mt-4 text-gray-500 dark:text-gray-400"> {block?.description || ""} </p> <Link href={block?.link?.url || "/"}> <a className="inline-flex h-12 px-6 mt-8 text-sm font-medium text-white bg-gray-900 dark:bg-gray-50 rounded-md shadow-sm hover:bg-gray-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950" target="_blank" > Learn More </a> </Link> </div> </div> </section> ); } ``` 3. Update the `HomePageContent` component to pass the correct data to the HeroSection: ```tsx // components/HomePageContent.tsx import { Block } from "@/lib/types"; import HeroSection from "./HeroSection"; interface Props { blocks?: Block[]; } export default function HomePageContent({ blocks = [] }: Props) { return ( <div className="space-y-12"> {blocks.map((block, index) => ( <HeroSection key={index} block={block} /> ))} </div> ); } ``` 4. Update the `HomePageContentLoader` component to pass the correct data to the HeroSection: ```tsx // components/HomePageContentLoader.tsx import { Block } from "@/lib/types"; import Skeleton from "./Skeleton"; import HeroSection from "./HeroSection"; interface Props { loading?: boolean; } export default function HomePageContentLoader({ loading = false }: Props) { return ( <div className="space-y-12"> {loading ? ( Array.from({ length: 3 }).map((_, index) => ( <Skeleton key={index} /> )) ) : ( <HomePageContent blocks={[]} /> )} </div> ); } ``` Now, our Hero Section is using the Next.js Image component for optimized image handling. ## Building the Features Section Next, let's build the Features Section by modeling data in Strapi and creating corresponding components in Next.js. We will display features dynamically from the Strapi CMS. 1. Model the Features Section data in Strapi: - Go to Content-Type Builder (http://localhost:1337/admin/plugins/content-type-builder) - Click on "Create new collection type" and name it "Features" - Add the following fields: - Title (Single Line Text) - Description (Rich Text) - Image (Media Library) - Link (URL) 2. Create a custom StrapiImage component inside the `components` folder: ```tsx // components/StrapiImage.tsx import Image from "next/image"; interface Props { image: any; } export default function StrapiImage({ image }: Props) { return ( <Image src={image?.data?.attributes?.url || "/"} alt={image?.data?.attributes?.alternativeText || ""} width={image?.data?.attributes?.width || 0} height={image?.data?.attributes?.height || 0} layout="fill" objectFit="cover" /> ); } ``` 3. Create a `FeatureItem` component inside the `components` folder: ```tsx // components/FeatureItem.tsx import { Block } from "@/lib/types"; import Image from "next/image"; import Link from "next/link"; import { cn } from "@/lib/utils"; import StrapiImage from "./StrapiImage"; interface Props { feature: any; } export default function FeatureItem({ feature }: Props) { return ( <div className="flex flex-col items-center space-y-4"> {feature?.image && ( <StrapiImage image={feature.image} /> )} <h3 className="text-2xl font-bold text-gray-900 dark:text-gray-100"> {feature?.title || ""} </h3> <p className="max-w-md text-center text-gray-500 dark:text-gray-400"> {feature?.description || ""} </p> {feature?.link && ( <Link href={feature.link.url}> <a className="inline-flex h-12 px-6 mt-8 text-sm font-medium text-white bg-gray-900 dark:bg-gray-50 rounded-md shadow-sm hover:bg-gray-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-950" target="_blank" > Learn More </a> </Link> )} </div> ); } ``` 4. Create a `FeaturesSection` component inside the `components` folder: ```tsx // components/FeaturesSection.tsx import { Block } from "@/lib/types"; import FeatureItem from "./FeatureItem"; interface Props { block: Block; } export default function FeaturesSection({ block }: Props) { return ( <section className="bg-gray-100 dark:bg-gray-900"> <div className="max-w-screen-2xl px-4 py-16 mx-auto sm:px-6 lg:px-8"> <h2 className="text-3xl font-bold text-gray-900 dark:text-gray-100"> {block?.title || ""} </h2> <div className="grid grid-cols-1 gap-8 mt-8 md:grid-cols-2 lg:grid-cols-3"> {block?.features.map((feature, index) => ( <FeatureItem key={index} feature={feature} /> ))} </div> </div> </section> ); } ``` 5. Update the `HomePageContent` component to pass the correct data to the Features Section: ```tsx // components/HomePageContent.tsx import { Block } from "@/lib/types"; import HeroSection from "./HeroSection"; import FeaturesSection from "./FeaturesSection"; interface Props { blocks?: Block[]; } export default function HomePageContent({ blocks = [] }: Props) { return ( <div className="space-y-12"> {blocks.map((block, index) => ( <HeroSection key={index} block={block} /> ))} {blocks.map((block, index) => ( <FeaturesSection key={index} block={block} /> ))} </div> ); } ``` 6. Update the `HomePageContentLoader` component to pass the correct data to the Features Section: ```tsx // components/HomePageContentLoader.tsx import { Block } from "@/lib/types"; import Skeleton from "./Skeleton"; import HeroSection from "./HeroSection"; import FeaturesSection from "./FeaturesSection"; interface Props { loading?: boolean; } export default function HomePageContentLoader({ loading = false }: Props) { return ( <div className="space-y-12"> {loading ? ( Array.from({ length: 3 }).map((_, index) => ( <Skeleton key={index} /> )) ) : ( <HomePageContent blocks={[]} /> )} </div> ); } ``` Now, our Features Section is displaying features dynamically from the Strapi CMS. ## Displaying Dynamic Meta Data Next, let's examine how to get our metadata from Strapi and display it on our layout.tsx page. 1. Create a new function called `getGlobalPageMetadata` inside the `data/loaders.ts` file: ```tsx // data/loaders.ts import fetchData from "./fetch"; export async function getHomePageData() { const url = new URL("/api/home-page", baseUrl); url.search = qs.stringify({ populate: { blocks: { populate: { image: { fields: ["url", "alternativeText"], }, link: { populate: true, }, feature: { populate: true, }, }, }, }, }); return await fetchData(url.href); } export async function getGlobalPageMetadata() { const url = new URL("/api/global", baseUrl); url.search = qs.stringify({ fields: ["title", "description"], }); return await fetchData(url.href); } ``` In the function above, we ask Strapi to return only the title and description, which are the only data we need for our metadata. The response will look like the following: ```json { "data": { "id": 4, "documentId": 'fyj7ijjnkxy75h1cbusrafj2', "title": 'Global Page', "description": 'Responsible for our header and footer.' } } ``` 2. Update the `generateMetadata` function inside the `layout.tsx` file to use the `getGlobalPageMetadata` function: ```tsx // app/layout.tsx import type { Metadata } from "next"; import localFont from "next/font/local"; import "./globals.css"; import { getHomePageData, getGlobalPageMetadata } from "@/data/loaders"; import { Header } from "@/components/custom/header"; import { Footer } from "@/components/custom/footer"; const geistSans = localFont({ src: "./fonts/GeistVF.woff", variable: "--font-geist-sans", weight: "100 900", }); const geistMono = localFont({ src: "./fonts/GeistMonoVF.woff", variable: "--font-geist-mono", weight: "100 900", }); export async function generateMetadata(): Promise<Metadata> { const metadata = await getGlobalPageMetadata(); return { title: metadata?.data?.title ?? "Epic Next Course", description: metadata?.data?.description ?? "Epic Next Course", }; } export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { const globalData = await getGlobalData(); console.dir(globalData, { depth: null }); return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`} > <Header data={globalData.data.header} /> {children} <Footer data={globalData.data.footer} /> </body> </html> ); } ``` Now, our metadata is dynamically set from our Strapi API. ## Top Header and Footer Let's create our top header and footer using global data fetched from Strapi. We will leverage Strapi to manage and fetch global data like logo texts and social links. 1. Update the `getGlobalData` function inside the `data/loaders.ts` file: ```tsx // data/loaders.ts import fetchData from "./fetch"; export async function getHomePageData() { const url = new URL("/api/home-page", baseUrl); url.search = qs.stringify({ populate: { blocks: { populate: { image: { fields: ["url", "alternativeText"], }, link: { populate: true, }, feature: { populate: true, }, }, }, }, }); return await fetchData(url.href); } export async function getGlobalPageMetadata() { const url = new URL("/api/global", baseUrl); url.search = qs.stringify({ fields: ["title", "description"], }); return await fetchData(url.href); } export async function getGlobalData() { const url = new URL("/api/global", baseUrl); url.search = qs.stringify({ populate: "*", }); return await fetchData(url.href); } ``` In the function above, we ask Strapi to return all data for our global page, including the title and description fields that we used earlier for dynamic metadata. The response will look like the following: ```json { "data": { "id": 2, "documentId": 'fyj7ijjnkxy75h1cbusrafj2', "title": 'Global Page', "description": 'Responsible for our header and footer.', "header": { "data": { "id": 1, "attributes": { "title": "Epic Next Course", "description": "The best place to learn Next.js.", "socialLinks": [ { "id": 1, "attributes": { "url": "https://github.com/vercel/next.js", "icon": "GitHub" } }, { "id": 2, "attributes": { "url": "https://www.linkedin.com/company/vercel/", "icon": "LinkedIn" } } ] } } }, "footer": { "data": { "id": 2, "attributes": { "title": "Epic Next Course", "description": "The best place to learn Next.js.", "socialLinks": [ { "id": 1, "attributes": { "url": "https://github.com/vercel/next.js", "icon": "GitHub" } }, { "id": 2, "attributes": { "url": "https://www.linkedin.com/company/vercel/", "icon": "LinkedIn" } } ] } } } } } ``` 2. Create a `SocialLinkItem` component inside the `components` folder: ```tsx // components/SocialLinkItem.tsx import { Block } from "@/lib/types"; import Image from "next/image"; import Link from "next/link"; import { cn } from "@/lib/utils"; interface Props { socialLink: any; } export default function SocialLinkItem({ socialLink }: Props) { return ( <li> <Link href={socialLink.url}> <a target="_blank"> {socialLink?.icon && ( <Image src={`/icons/${socialLink.icon}.svg`} alt={socialLink.icon} width={24} height={24} /> )} </a> </Link> </li> ); } ``` 3. Create a `SocialLinksList` component inside the `components` folder: ```tsx // components/SocialLinksList.tsx import { Block } from "@/lib/types"; import SocialLinkItem from "./SocialLinkItem"; interface Props { socialLinks?: any[]; } export default function SocialLinksList({ socialLinks = [] }: Props) { return ( <ul className="flex space-x-4"> {socialLinks.map((socialLink, index) => ( <SocialLinkItem key={index} socialLink={socialLink} /> ))} </ul> ); } ``` 4. Create a `Header` component inside the `components/custom` folder: ```tsx // components/custom/Header.tsx import { Block } from "@/lib/types"; import Image from "next/image"; import Link from "next/link"; import SocialLinksList from "../SocialLinksList"; interface Props { data?: any; } export default function Header({ data = {} }: Props) { return ( <header className="bg-gray-100 dark:bg-gray-900"> <div className="max-w-screen-2xl px-4 py-6 mx-auto sm:px-6 lg:px-8"> <Link href="/"> <a> {data?.attributes?.title && ( <Image src={`/icons/${data.attributes.icon}.svg`} alt={data.attributes.icon} width={32} height={32} /> )} </a> </Link> <div className="flex justify-center flex-1"> {data?.attributes?.description && ( <p className="text-gray-500 dark:text-gray-400"> {data.attributes.description} </p> )} </div> <SocialLinksList socialLinks={data?.attributes?.socialLinks || []} /> </div> </header> ); } ``` 5. Create a `Footer` component inside the `components/custom` folder: ```tsx // components/custom/Footer.tsx import { Block } from "@/lib/types"; import Image from "next/image"; import Link from "next/link"; import SocialLinksList from "../SocialLinksList"; interface Props { data?: any; } export default function Footer({ data = {} }: Props) { return ( <footer className="bg-gray-100 dark:bg-gray-900"> <div className="max-w-screen-2xl px-4 py-8 mx-auto sm:px-6 lg:px-8"> {data?.attributes?.title && ( <Image src={`/icons/${data.attributes.icon}.svg`} alt={data.attributes.icon} width={32} height={32} /> )} <div className="flex justify-center flex-1"> {data?.attributes?.description && ( <p className="text-gray-500 dark:text-gray-400"> {data.attributes.description} </p> )} </div> <SocialLinksList socialLinks={data?.attributes?.socialLinks || []} /> </div> </footer> ); } ``` 6. Update the `generateMetadata` function inside the `layout.tsx` file to use the `getGlobalData` function: ```tsx // app/layout.tsx import type { Metadata } from "next"; import localFont from "next/font/local"; import "./globals.css"; import { getHomePageData, getGlobalData } from "@/data/loaders"; import { Header } from "@/components/custom/header"; import { Footer } from "@/components/custom/footer"; const geistSans = localFont({ src: "./fonts/GeistVF.woff", variable: "--font-geist-sans", weight: "100 900", }); const geistMono = localFont({ src: "./fonts/GeistMonoVF.woff", variable: "--font-geist-mono", weight: "100 900", }); export async function generateMetadata(): Promise<Metadata> { const globalData = await getGlobalData(); return { title: globalData?.data?.attributes?.title || "Epic Next Course", description: globalData?.data?.attributes?.description || "The best place to learn Next.js.", }; } export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { const globalData = await getGlobalData(); console.dir(globalData, { depth: null }); return ( <html lang="en"> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`} > <Header data={globalData?.data?.attributes?.header || {}} /> {children} <Footer data={globalData?.data?.attributes?.footer || {}} /> </body> </html> ); } ``` Now, our top header and footer are using global data fetched from Strapi. ## Handling Loading, Not Found, and Error Pages Finally, let's handle loading, not found, and error pages in our application. 1. Create a `loading.tsx` file inside the `pages` folder: ```tsx // pages/loading.tsx import { Block } from "@/lib/types"; import Skeleton from "./Skeleton"; import HomePageContentLoader from "./HomePageContentLoader"; interface Props { blocks?: Block[]; } export default function Loading({ blocks = [] }: Props) { return ( <div className="space-y-12"> <HomePageContentLoader loading /> </div> ); } ``` 2. Create a `not-found.tsx` file inside the `pages` folder: ```tsx // pages/not-found.tsx import { Block } from "@/lib/types"; import Skeleton from "./Skeleton"; import HomePageContentLoader from "./HomePageContentLoader"; interface Props { blocks?: Block[]; } export default function NotFound({ blocks = [] }: Props) { return ( <div className="space-y-12"> <HomePageContentLoader loading /> </div> ); } ``` 3. Create a `error.tsx` file inside the `pages` folder: ```tsx // pages/error.tsx import { Block } from "@/lib/types"; import Skeleton from "./Skeleton"; import HomePageContentLoader from "./HomePageContentLoader"; interface Props { blocks?: Block[]; } export default function Error({ blocks = [] }: Props) { return ( <div className="space-y-12"> <HomePageContentLoader loading /> </div> ); } ``` Now, our application is handling loading, not found, and error pages. That's it! We have successfully continued building our landing page using Next.js and Strapi by refactoring the Hero Section to use the Next.js Image component for optimized image handling, building the Features Section by modeling data in Strapi and creating corresponding components in Next.js, displaying dynamic metadata from Strapi on our layout.tsx page, creating a top header and footer using global data fetched from Strapi, and handling loading, not found, and error pages in our application. In the next part of this tutorial series, we will continue building our landing page by adding user authentication with NextAuth.js and integrating it with our existing Strapi API.
Mar 19, 2024 6,800 words in the original blog post.
In this tutorial, we will create a multi-language blog using Strapi, an open-source headless CMS, and Next.js, a dynamic React framework. We will set up our development environment, create content models, manage entries, enable GraphQL for seamless interactions, and build the frontend with Next.js to display our blogs in multiple languages. The combination of Strapi and Next.js allows us to deliver dynamic, multi-language blogs while maintaining data integrity.
Mar 14, 2024 1,585 words in the original blog post.
In this tutorial, we learn how to create a Hero Section component using Strapi and Next.js. We start by building out the structure of our Hero Section in Strapi, then fetch that data in our Next.js application and display it on the homepage. Along the way, we also discuss caching strategies for Next.js applications. The tutorial concludes with a plan to create a StrapiImage component and finish up the Hero Section before moving on to the Features Section in the next post.
Mar 12, 2024 3,714 words in the original blog post.
In this tutorial, we begin building a home page using Next.js and Strapi. We create a Hero Component and Features Component using Dynamic Zone to allow Strapi admins to choose which components they want to use. We then fetch data from the Strapi API and build out those same components within our Next JS app. The goal is to display the Hero Section and Features Sections on our Next application.
Mar 12, 2024 2,959 words in the original blog post.
This tutorial guides you through using Strapi, an open-source Node.js based headless CMS, and webhooks to create real-time notifications for a blog application. The process involves setting up content type models in Strapi, configuring the email plugin, creating a notify controller and routes, and finally setting up a webhook to trigger emails when new blog posts are created. By the end of this tutorial, you will have learned how to integrate real-time notifications into your applications using Strapi and webhooks.
Mar 06, 2024 1,057 words in the original blog post.
In this tutorial, we will build an app to summarize YouTube videos using Next JS 14 and Strapi. The app will use AI to generate video summaries and allow users to create notes around the videos they watch. We will leverage the power of Tailwind and ShadcnUI for styling and Strapi Headless CMS for managing data and authentication. The project will cover important features of Next JS 14, such as server-side rendering, static generation, and incremental static regeneration. Additionally, we will explore middlewares, policies, routes, controllers, and services in Strapi.
Mar 05, 2024 2,179 words in the original blog post.
In this tutorial series, we will build a video summarization app using Next.js 15 and Strapi headless CMS. The app aims to generate summaries of YouTube videos with the help of AI, saving users time by allowing them to read summaries instead of watching entire videos. We will leverage the features of Next.js 15, such as server components and server actions, and use Tailwind and ShadcnUI for styling. Strapi will be used to manage data and authentication. The app will have a landing page with top navigation, hero sections, benefits section, and footer, along with a dashboard for summaries and notes. We will also cover important parts of Next.js and some features related to Strapi, such as middlewares, policies, routes, controllers, and services.
Mar 05, 2024 2,565 words in the original blog post.