Goal
Create a custom content type in Acquia CMS and render the content in Next.js using the next-acms.
Prerequisites
-
You have a working build of Acquia CMS for headless development setup.
-
You next.js project is connected to your Acquia CMS headless install (see tutorial)
Overview
Here are the step we’ll cover in this tutorial:
- Define a Book content type with fields in Acquia CMS and expose the content to Next.js.
- Build a /books page to display all published books in Next.js and link it in the.
- Build a book page in Next.js to view the content using React components.
-
Create the Book Content Type
For our example, we are going to use a Book content type with the following fields:
- Title (title) - Title field
- Body (body) - Text (formatted, long)
- Author (field_display_author) - Reuse existing field from Article content type.
- Image (field_book_image) - Media (Image)
-
Create Pathauto pattern
Next.js uses the decoupled router module in Drupal to find content (resource) based on its URL alias. For this to work, the node always needs a URL alias. We can ensure this by setting a Pathauto pattern.
Create a Pathauto pattern for Book with the following pattern: books/Next.js on Acquia: Customizing content types
Once the content types have been created, you may go ahead and fill in some sample content.
-
Set Book as a Next.js Entity Type
In Acquia CMS, go to Administration > Configuration > Web services > Next.js sites > Entity Types and add the Book content type with a “Site selector” site resolver.
-
Add books to the Drupal menu
As Next.js pulls Drupal's menu through, if we want to see our books page in the Next.js navigation, we must add it to the Menu.
Create a new menu item in the Main Navigation menu with a title of "Books" and a URL of
/books
Note:
/books
will not exist in Drupal but Next.js will be able to find a route for it within the Next.js application. -
A page with all books
Now that our content types are created and we have some default content, we can pull this content into our Next.js site and display it. To do this, we are going to use the Next.js for Drupal framework that interacts with Drupal’s JSON:API implementation.
-
Fetch a list of books
Let’s start by creating a new page to show all our books.
- Create a new file called
books.tsx
underpages/books.tsx
- Next.js requires two function to fetch and display data:
getStaticProps
to fetch data- a React page component to display data
Let’s add a scaffolded React component:
import { Layout } from "components/layout"import { PageHeader } from "components/page-header"export default function BooksPage({ books, menus }) { return ( No content found. )}
Here, we’re importing the
Layout
andPageHeader
React components and using them inside ourBooksPage
component to render our page template. Take a look atpages/articles.tsx
for a similar layout. Here we’re looking to keep the look and feel consistent across the site. The Acquia CMS starter template comes with Tailwind CSS out of the box. We can use utility classes to style our book collection. Lets pull our book content from Acquia CMS by adding a function calledgetStaticProps
with the following:import { getResourceCollectionFromContext } from "next-drupal"import { getMenus } from "lib/get-menus"export async function getStaticProps(context) { const books = await getResourceCollectionFromContext("node--book", context) return { props: { books, menus: await getMenus(), }, }}
getResourceCollectionFromContext
is a helper function from next-drupal. Here we are telling next-drupal to fetch a collection ofnode-book
resources.The
props
returned fromgetStaticProps
are passed then to ourBooksPage
component.If you add a
console.log(books)
in yourBooksPage
component you should see it log the sample books you created above.export default function BooksPage({ books, menus }) { // console.log(books) // …
If you look at the content of books, you will notice
getResourceCollectionFromContext
returns thenode—books
will all the fields. Since we only care about some of the fields, let’s tell JSON:API to only return a subset of the fields.import { DrupalJsonApiParams } from "drupal-jsonapi-params"const books = await getResourceCollectionFromContext("node--book", context, { // For the node--book, only fetch the following fields. params: new DrupalJsonApiParams() .addInclude(["field_book_image.image", "field_display_author"]) .addFields("node--book", [ "title", "body", "path", "field_display_author", "field_book_image", ]) .getQueryObject(),})
Next, let’s take a look at
field_display_author
andfield_book_image
. These are entity reference fields and don’t include the metadata we need on the book entity so we must tell the JSON:API to include these entities in the response. We do so with the addInclude function. - Create a new file called
-
Display a list of books
Once we have our books data, we can go ahead and render a list of books in the React page component. Lets update
BooksPage
to look like this:export default function BooksPage({ books, menus }) { return ( {books?.length ? ( {books.map((book) => ( ))} ) : ( No content found. )} )}
This introduces a new React component that doesn’t exist yet:
NodeBookTeaser
. We need to create this component before this page will render without error.Create a new file called
components/node--book.tsx
with the following component in it:import Link from "next/link"import { MediaImage } from "components/media--image"export function NodeBookTeaser({ node }) { return ( {node.field_book_image & ( )} {node.field_display_author?.title ? ( {node.field_display_author?.title} ) : null} {node.title} {node.body && ( className="text-xs text-gray-600" dangerouslySetInnerHTML={ { __html: node.body.processed } } /> )} )}Here you can see the React component is doing a number of things: Its templating with HTML tags like , , and . Its passing objects onto other React components to render ( and ) Its using tailwind CSS inline to style HTML components Its working with the book node object to conditionally render components (similar to how would in twig). Note MediaImage is a component that comes with the Acquia CMS starter kit for rendering images managed by the Media module. Now with this component defined, we must import it in our Books page to use it: import { NodeBookTeaser } from "components/node--book"export default function BooksPage({ books, menus }) {…That’s it. We now have a Books page built with data from Drupal. But if you click on a book link, you’ll find it doesn’t work yet. A page for every book The next step is to create a page for every book. This page will display additional information about each book. The starter comes with a pages/[[...slug]].tsx page. This is a special page. It acts as an entry point for content or nodes that are created on Drupal. Let’s tell this page to build pages for node—book resources. To do so there are a number of places where we have to update the slug: Import the node--book component (like we did in pages/books.tsx) Add node--book to the CONTENT_TYPES constant Update the NodePage component to use a NodeBook component (which we’ll need to create) for book type nodes. Specify the JSON:API query modifying parameters for the book content type. So lets start by importing node--book component we previously created. You can place this at the top of the file: import { NodeBook } from "components/node--book"This will make Next.js break because NodeBook doesn’t actually exist yet. We created NodeBookTeaser but not NodeBook. So lets create a stub in components/node--book.tsx for now that we can revisit later: export function NodeBook({ node }) { return {node.title}}Next, Update the CONTENT_TYPES variable and add node—book. const CONTENT_TYPES = [ "node--page", "node--article", "node--event", "node--person", "node--place", "node--book" // ]Then, update getStaticProps to add a condition for the node--book type and load related author and image data. export async function getStaticProps( context): Promise> { // ... if (type === "node--place") { params.addInclude(["field_place_image.image"]) } if (type === "node--book") { params.addInclude(["field_display_author", "field_book_image.image"]) } const node = await getResourceFromContext(type, context, { params: params.getQueryObject(), }) // ...}Then, update the NodePage component to render the NodeBook component for node—book resources. Now we have the NodeBook component correctly routed lets revisit it in components/node--book.tsx and revise the output: That’s it. If you visit the /books page and click on a book title, you should be taken to the book page. To learn more about data fetching and rendering, refer to the Next-Drupal documentation. Additional Resources Acquia CMS Starter Kit Acquia CMS Next.js for Drupal documentation Tutorial: Next.js on Acquia: Next.js startkit for Acquia CMS Tutorial: Next.js on Acquia: Setting up Acquia CMS