We had the chance to explore the new features of Next.js 13 in a Proof-of-Concept project just recently. While it will still take some time to finish everything up and make a new Zebra release, we want to share a preview of the new features and how they will make the combination of Next.js and Neos CMS with Zebra even more powerful.
What is Zebra? Read our introduction post to get an overview of how to integrate Neos CMS with Next.js with full editing capabilities in the Neos UI.
What does Next.js 13 offer?
The biggest change is the new app router, which offers a lot of new possibilities for loading data in routes. Before, with the page router, each route could decide to be fully static, server-side rendered or additionally using client-side data loading. By opting in to the new app router (which can work besides the page router), each component can decide how it wants to load data. This leverages the new React Server Components (RSC) feature, which allows to keep certain React components only on the server and stream output them to the client.
- Fine-grained control over data loading in routes and individual components (with RSC)
- Ship less JS to the browser with React Server Components
- More control over performance with streaming of data to the browser
While this is all great news and we look forward to use the app router in our projects, it also meant to go back on the drawing-board for Zebra and re-think how we can integrate Neos CMS with Next.js.
Doing a Proof-of-Concept (PoC) for exploration
To explore the new features, we had the chance to do a PoC project with digital agency Format D for integrating Neos CMS in a Next.js application that is currently being built. The goal was to find out how we can use the new app router to integrate Neos CMS with Next.js. We already had a feeling that it should be possible to keep the full editing capabilities of Neos CMS and our approach to providing hooks and simple components in the Zebra package to render node types completely via Next.js.
As it turned out when adapting the existing package to the app router we needed to be more specific about code for the client
and server. With the "use client"
pragma we can tell Next.js to render a component on the client. While being still pre-rendered
on the server, this will ship JS for the component to the browser and re-hydrates the component. Server components in contrast are
the default way of data fetching and streaming the output to the browser. The JavaScript code does not need to be shipped to the browser and
the component is only rendered on the server.
We quickly settled on the goal to enable rendering CMS components purely with server components (at least when not editing).
One issue we faced was, that there is no support for createContext
with server components. The rationale is that
there is no way to re-render the component on the server when the context changes. As we currently use the context to
provide the fetched data from the Neos content API to the components (e.g. via the useNode()
hook), we needed to find
a replacement.
To our surprise this is still relatively uncharted territory as there weren't many examples on how to use context with server components - and if it is possible at all. We didn't want to pass props down the component tree, as this would mean to pass the data through all components that are rendered on the server. This would mean some unneeded overhead and would clutter presentational components.
Turns out React 18 does support context with server components, but it is not yet documented.
The createServerContext
function is available in the react
package and can be used to create a context that is
consumable via useContext
as usual:
import { createServerContext } from 'react';
import { NeosServerContextProps } from '../../types';
export const NeosServerContext =
createServerContext<NeosServerContextProps>(
'neosDataContext', {}
);
You don't want to put all the data into the context though, as it will be shipped to the browser to have it available
for client components (which can be included by server component).
This is where the data cache - respectively the new cache
function comes in handy. So we settled on the following solution:
- We keep the information about the current document (route path / node context path) and node in the server context - but only as identifiers for lookup.
- Each server component that needs node data uses a helper to fetch the data for the current document and resolves the current node via the context identifier.
- The
cache
function will make sure that no duplicate fetch calls are made for the same document in a single request.
This way we can keep the data fetching logic in the server components and don't need to pass props down the component tree. The code for content components looks very similar to the current Zebra implementation:
import { useNode, ContentComponent, Editable } from '@networkteam/zebra/server';
import Headline from '../ui/Headline';
const ContentHeadline = async () => {
const node = await useNode()();
return (
<ContentComponent>
<Headline as={node?.properties.hierarchy} size={node?.properties.size}>
<Editable property="title" />
</Headline>
</ContentComponent>
);
};
export default ContentHeadline;
Tip: Beware of using multiple hooks mixed with
await
. This can lead to unexpected results, as it violates the rules of hooks. Just use an intermediateloader
variable after calling all hooks unconditionally.
Et voilà, we can now render content from Neos with server components and the new app router and still have the full editing capabilities of Neos CMS.
The approach feels very good and will in turn be superior to the page based router, as we can reduce the amount of data (JSON) that needs to be shipped to the browser and skip re-hydration for much of the content. You still have the option to use client components for certain parts of the page as needed for more interactive parts.
Using Neos content in any Next.js app
Another main question of the PoC was, if we can integrate Neos content in any Next.js app route - e.g. one that has static content and displays some data.
The answer is: Yes, we can!
We are still working on the details, but the idea is to provide an EditableDocument
component (the name is subject to change) that can be used to render Neos content in an existing Next.js app route and make it editable in the Neos UI:
export default function Home({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
return (
<EditableDocument
routePath="/"
contextNodePath={getContextNodePath(searchParams)}
>
<main>
<h1>Welcome to our shop!</h1>
<ContentCollection nodeName="promotions" />
<ProductOverview />
</main>
</EditableDocument>
);
}
The idea is, that every editable route in Next.js will have a corresponding document node in Neos CMS. There will be a mapping from these documents to the Next.js route paths and we will redirect from the node preview to the corresponding Next.js route and provide the current node context path (with user workspace and dimensions) when in the Neos UI.
This way we can use Neos CMS to manage content for "embedded" content, but also keep it for slug based routes where new pages can be freely created via Neos. Coupled with the new layouts in the app router this gives a lot of flexibility for composing front-ends with Next.js.
What's next?
We'll hopefully release a new version of Zebra with support for the new app router and server components in the next weeks. This will provide full support for the page based router and the new app router. There'll be only some minor updates to the import paths to separate server and client code.
Thanks again to Format D for the opportunity to explore these new features in a PoC project. We are happy to collaborate with other agencies and companies to explore these exciting new possibilities for Neos CMS paired with Next.js.
If you are interested, feel free to get in contact with me.