Getting Started With Next.js and Strapi 5: beginner's guide.
In this tutorial, we will build a dynamic website using Next.js 14 and Strapi 5. We will create a team member detail page that displays different types of content blocks such as spoilers, testimonials, and rich text. To achieve this, we will use the Blocks API provided by Strapi to manage our content blocks. First, let's install the required dependencies: ```bash npm install strapi @strapi/create-strapi-app @strapi/utils @strapi/types @strapi/react-sdk @strapi/react-model @strapi/react-form @strapi/react-query @strapi/react-ui-test @strapi/react-bootstrap ``` Next, let's create a new Strapi project: ```bash npx create-strapi-app strapi-nextjs --quickstart ``` Now, let's install the Next.js 14 app and connect it to our Strapi backend: ```bash npx create-next-app@latest --ts cd nextjs-app npm run dev ``` Inside the `pages/team/[slug].tsx` file, let's add the following code: ```typescript import { useRouter } from "next/router"; import getTeamMember from "../../../utils/getTeamMember"; interface TeamPageBlock { __component: string; id: number; } const BlockRenderer = ({ block }: { block: TeamPageBlock }) => { switch (block.__component) { case "blocks.spoiler": return <div>Spoiler Block</div>; case "blocks.testimonial": return <div>Testimonial Block</div>; case "blocks.rich-text": return <div>Rich Text Block</div>; default: return null; } }; const TeamMemberDetail = ({ params }: { params: { slug: string } }) => { const router = useRouter(); if (!router.isReady) return <p>Loading...</p>; const { slug } = router.query; if (!slug) return <p>No member found</p>; const teamMember = (await getTeamMember(slug)) as UserProfile; return ( <div> {teamMember.blocks.map((block: TeamPageBlock) => ( <BlockRenderer key={block.id} block={block} /> ))} </div> ); }; export default TeamMemberDetail; ``` For the above code to work, we must create our new BlockRenderer component and all the other components we use on our Team Member Detail page. Let's do that now. Let's create a new folder in our app/components folder and name it blocks. Let's create a new file and name it index.tsx. Now, let's create the following components in our blocks folder: - spoiler-block.tsx - testimonial-block.tsx - rich-text-block.tsx Here is what the spoiler-block.tsx file should look like: ```typescript import { useState } from "react"; export interface SpoilerBlock { __component: "blocks.spoiler"; id: number; title: string; content: string; } export function SpoilerBlock({ block }: { block: SpoilerBlock }) { const [isExpanded, setIsExpanded] = useState(false); return ( <div className="w-full mb-4 rounded-lg overflow-hidden shadow-md transition-all duration-300 ease-in-out"> <button className={`w-full flex justify-between items-center p-4 bg-gray-100 hover:bg-gray-200 transition-colors duration-300 ease-in-out ${ isExpanded ? "bg-gray-200" : "" }`} onClick={() => setIsExpanded(!isExpanded)} aria-expanded={isExpanded} > <span className="text-lg font-semibold text-gray-800"> {block.title} </span> <span className={`text-2xl text-gray-600 transition-transform duration-300 ease-in-out ${ isExpanded ? "transform rotate-180" : "" }`} > {isExpanded ? "-" : "+"} </span> </button> <div className={`overflow-hidden transition-all duration-300 ease-in-out ${ isExpanded ? "max-h-[1000px]" : "max-h-0" }`} aria-hidden={!isExpanded} > <div className="p-4 bg-white text-gray-700 leading-relaxed"> {block.content} </div> </div> </div> ); } ``` Here is what the testimonial-block.tsx file should look like: ```typescript import Image from "next/image"; export interface TestimonialBlock { __component: "blocks.testimonial"; id: number; authorName: string; quote: string; photo: { id: number; documentId: string; alternativeText: string | null; name: string; url: string; }; } export function TestimonialBlock({ block }: { block: TestimonialBlock }) { const imageUrl = `${process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:1337"}${block?.photo?.url}`; return ( <figure className="relative bg-gray-100 rounded-lg border border-gray-200 overflow-hidden my-6"> <div className="grid grid-cols-1 md:grid-cols-3"> <div className="relative h-64 md:h-full col-span-1"> <Image src={imageUrl} alt={block.photo.alternativeText || block.authorName} layout="fill" objectFit="cover" className="w-full h-full object-center" /> </div> <div className="p-8 col-span-2 flex flex-col justify-center"> <blockquote className="relative"> <svg className="absolute top-0 left-0 transform -translate-x-6 -translate-y-8 h-16 w-16 text-gray-300 opacity-50" fill="currentColor" viewBox="0 0 32 32" aria-hidden="true" > <path d="M9.352 4C4.456 7.456 1 13.12 1 19.36c0 5.088 3.072 8.064 6.624 8.064 3.36 0 5.856-2.688 5.856-5.856 0-3.168-2.208-5.472-5.088-5.472-.576 0-1.344.096-1.536.192.48-3.264 3.552-7.104 6.624-9.024L9.352 4zm16.512 0c-4.8 3.456-8.256 9.12-8.256 15.36 0 5.088 3.072 8.064 6.624 8.064 3.264 0 5.856-2.688 5.856-5.856 0-3.168-2.304-5.472-5.184-5.472-.576 0-1.248.096-1.44.192.48-3.264 3.456-7.104 6.528-9.024L25.864 4z" /> </svg> <p className="relative text-xl font-medium text-gray-900 mb-4"> {block.quote} </p> </blockquote> <figcaption className="font-semibold text-indigo-600 mt-2"> {block.authorName} </figcaption> </div> </div> </figure> ); } ``` Here is what the rich-text-block.tsx file should look like: ```typescript import { BlocksRenderer, type BlocksContent } from "@strapi/blocks-react-renderer"; import Image from "next/image"; export interface RichTextBlock { __component: "blocks.rich-text"; id: number; content: BlocksContent; } // This renderer is using Strapi's Rich Text renderer. // https://github.com/strapi/blocks-react-renderer export function RichTextBlock({ block }: { block: RichTextBlock }) { return ( <div className="richtext"> <BlocksRenderer content={block.content} blocks={{ image: ({ image }) => { console.log("image", image); if (!image) return null; return ( <div className="my-4 flex justify-center"> <Image src={image.url} width={image.width || 800} height={image.height || 600} alt={image.alternativeText || ""} className="rounded-lg shadow-md h-[300px] w-full object-cover" /> </div> ); }, }} /> </div> ); } ``` This component is using Strapi's Rich Text renderer. You can learn more about it here; To style our Rich Text component, we use Tailwind CSS. Here is what I added in the global.css file: ```css /* Rich Text Block Start */ .richtext h1, .richtext h2, .richtext h3, .richtext h4, .richtext h5, .richtext h6 { @apply font-bold leading-tight; } .richtext h1 { @apply text-4xl mb-6 text-gray-900 dark:text-gray-100; } .richtext h2 { @apply text-3xl mb-4 text-gray-800 dark:text-gray-200; } .richtext h3 { @apply text-2xl mb-3 text-gray-700 dark:text-gray-300; } .richtext h4 { @apply text-xl mb-2 text-gray-600 dark:text-gray-400; } .richtext h5 { @apply text-lg mb-2 text-gray-600 dark:text-gray-400; } .richtext h6 { @apply text-base mb-2 text-gray-600 dark:text-gray-400; } .richtext p { @apply mb-4 text-gray-700 dark:text-gray-300 leading-relaxed; } .richtext blockquote { @apply border-l-4 border-blue-500 bg-blue-50 dark:bg-blue-900 dark:bg-opacity-20 italic my-8 p-4 rounded-r-lg; } .richtext a { @apply text-blue-600 dark:text-blue-400 hover:underline; } .richtext ul, .richtext ol { @apply mb-4 pl-8; } .richtext ul { @apply list-disc; } .richtext ol { @apply list-decimal; } /* Rich Text Block End */ ``` Nice, now that we have all of our components, let's add the following code in our components/blocks/index.tsx file: ```typescript import { RichTextBlock } from "./rich-text-block"; import { TestimonialBlock } from "./testimonial-block"; import { SpoilerBlock } from "./spoiler-block"; type TeamPageBlock = SpoilerBlock | TestimonialBlock | RichTextBlock; const blocks: Record<TeamPageBlock["__component"], React.ComponentType<{ block: TeamPageBlock }>> = { "blocks.spoiler": ({ block }: { block: TeamPageBlock }) => ( <SpoilerBlock block={block as SpoilerBlock} /> ), "blocks.testimonial": ({ block }: { block: TeamPageBlock }) => ( <TestimonialBlock block={block as TestimonialBlock} /> ), "blocks.rich-text": ({ block }: { block: TeamPageBlock }) => ( <RichTextBlock block={block as RichTextBlock} /> ), }; function BlockRenderer({ block }: { block: TeamPageBlock }) { const BlockComponent = blocks[block.__component]; return BlockComponent ? <BlockComponent block={block} /> : null; } export { BlockRenderer }; export type { TeamPageBlock }; ``` The TeamPageBlock type is a union of three block types: SpoilerBlock, TestimonialBlock, and RichTextBlock. Each block represents a distinct type of content that can be displayed. Each block component is imported and can be rendered based on the block data type. The blocks object maps a block's __component field (which is a string identifier like blocks.spoiler, blocks.testimonial, or blocks.rich-text) to the corresponding React component that should handle that block type. For example: When the block's __component is blocks.spoiler, it renders the SpoilerBlock component. Each block type is cast to the correct type using TypeScript's as keyword (block as SpoilerBlock) to ensure the correct component is used with the correct block data. The BlockRenderer component takes a TeamPageBlock object as a prop and renders the appropriate component based on the block's __component field. Inside this function, blocks[block.__component] retrieves the correct component for the given block. If a matching component is found, it is rendered; otherwise, null is returned (indicating no content is rendered for unrecognized block types). How the BlockRenderer Works When BlockRenderer is called with a block, it looks up the block.__component field in the blocks object to find the corresponding React component. It then renders the component, passing the block data to it. The specific component (e.g., SpoilerBlock, TestimonialBlock, or RichTextBlock) knows how to handle and render that particular block's data. Example Flow: A TeamPageBlock (e.g., a TestimonialBlock) is passed to BlockRenderer. BlockRenderer checks the block's __component field (e.g., blocks.testimonial). It finds the corresponding component (TestimonialBlock) in the blocks object. It renders the TestimonialBlock component, passing the block as a prop and displaying the block content. This pattern is useful for rendering dynamic content in which blocks are of different types and each type has its own specific rendering logic. Nice. Now that we have all our components, let's add some data for our team members in Strapi's admin panel. Then, check out our locally running Next.js app to see our new team member page.
Company
Strapi
Date published
Oct. 8, 2024
Author(s)
Paul Bratslavsky
Word count
6919
Language
English
Hacker News points
None found.