Next.js + Drupal for Incremental Static Regeneration and Preview mode.
Warning: this project is in early alpha. The API might change without notice.
The following demo sites are built from a single Drupal backend, Next.js for the frontend and styled with Reflexjs.
- Blog: http://next-example-blog.vercel.app/
- Marketing (with paragaphs): http://next-example-marketing.vercel.app/
To access the Drupal site and test the preview mode, you can clone this repository and run the demo sites on your local machine.
- Clone this repository
git clone https://github.com/arshad/next-drupal.git
- Install dependencies
yarn && composer install -d ./examples/drupal-site
- Copy
.env.example
to.env.local
:
cp examples/example-blog/.env.example examples/example-blog/.env.local
cp examples/example-marketing/.env.example examples/example-marketing/.env.local
- Generate a certificate for localhost (this is required to run your local sites with HTTPS for preview mode)
openssl req -x509 -out examples/certificates/localhost.crt -keyout examples/certificates/localhost.key \
-newkey rsa:2048 -nodes -sha256 \
-subj '/CN=localhost' -extensions EXT -config <(\
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
Double-click on the certificate to add it to your keychain.
- Then run
yarn dev
from the root to start the Drupal site and the Next.js sites.
yarn dev
-
Login to the Drupal site at http://localhost:8080 with username: admin and password: admin.
-
Visit http://localhost:8080/admin/config/people/simple_oauth to generate OAuth encryption keys. Enter
../oauth-keys
for the directory. -
Visit http://localhost:8080/admin/content to add, edit and preview content.
The blog site runs on https://localhost:3030 and the marketing site runs on https://localhost:3000.
The Next Drupal module is built to handle Incremental Static Regeneration and Preview Mode for your Next.js sites.
- Supports Incremental Static Regeneration: Your content changes are live instantly.
- Iframe preview: With site switcher and other preview modes.
- Multi-sites preview: Great for write once, publish everywhere.
- Supports revision previews, draft content and content moderation
- Extensible via plugins: Create your own site previewer and resolvers.
- Use Composer to install the Next module. From the root of your Drupal site, run the following command:
composer require drupal/next
-
Visit Extend in the Drupal admin.
-
Select the Next module and click Install.
The Next Drupal module, paired with the next-drupal
plugin, makes it easy to create Next.js preview routes.
To configure preview mode for an entity type, you must configure a Next.js site, a site resolver for the entity type and a OAuth Consumer.
A site resolver tells Drupal how to resolve the preview URL for an entity. Site resolvers are flexible, can handle multiple sites and work with entity reference fields.
- Visit /admin/config/services/next
- Click Add Next.js site
- Fill in the required information and click Save
- Visit /admin/config/services/next/entity-types
- Click Configure entity type
- Select the entity type from the list
- Select a Site resolver
- Click Save
If you visit an entity page, you should be able to see the Next.js site preview. See the next-drupal
plugin for more information on how to configure preview mode on the Next.js site.
To generate preview routes, the Next.js client uses the Client credentials grant for authentication. This is made possible using the (Simple OAuth)[https://www.drupal.org/project/simple_oauth] module.
Create a Drupal role
- Create a new Drupal role (example
Next site
) by visiting /admin/people/roles/add - Give the role the following permission:
- Bypass content access control
- View all revisions
- View user information
Create a user
Add a new user at /admin/people/create and assign it the role created above.
Note: When the Next.js is authenticated, it will be authenticated as this user.
Configure a consumer
- Visit /admin/config/people/simple_oauth
- Click Generate keys to generate encryption keys for tokens
- Visit /admin/config/services/consumer/add
- Fill in a Label, User (select the user created above), Secret and under Scopes, select the role create above
- Click Save
Important: note the client id (uuid) and the secret. This is going to be used as environment variables for the Next.js site.
The core of the Next module is built using plugins. This makes it easy to extend and customize how the preview works.
SitePreviewer plugins are responsible for rendering the preview for an entity type. Implement a @SitePreviewer
plugin to provide your own custom previewer.
Example: A plugin that renders a link to preview.
<?php
namespace Drupal\next\Plugin\Next\SitePreviewer;
use Drupal\Core\Entity\EntityInterface;
use Drupal\next\Plugin\SitePreviewerBase;
/**
* Provides a link to the preview page.
*
* @SitePreviewer(
* id = "link",
* label = "Link to preview",
* description = "Displays a link to the preview page."
* )
*/
class Link extends SitePreviewerBase {
/**
* {@inheritdoc}
*/
public function render(EntityInterface $entity, array $sites) {
$build = [];
foreach ($sites as $site) {
$build[] = [
'#type' => 'link',
'#title' => $this->t('Open preview'),
'#url' => $site->getPreviewUrlForEntity($entity),
];
}
return $build;
}
}
SitePreviewer plugins can provide their own configuration. See \Drupal\next\Plugin\ConfigurableSitePreviewerInterface
.
SiteResolver plugins are responsible for resolving next_site
entities for an entity. This can be based on the entity type, the bundle, the publish status or a field value.
Example: See \Drupal\next\Plugin\Next\SiteResolver\EntityReferenceField
.
The next-drupal
plugin provides helpers for consuming Drupal JSON API and for creating preview routes.
npm install --save next-drupal
- entity_type: the id of the entity_type
- bundle: the bundle for the entity
- options:
- params: JSON API params for filtering, includes, sorting..etc
- filter: a filter callback for filtering entities
Example:
export async function getStaticPaths() {
const paths = await getPathsForEntityType("node", "article", {
params: {
"filter[status]": 1,
},
})
return {
paths,
fallback: true,
}
}
- entity_type: the id of the entity_type
- bundle: the bundle for the entity
- context: GetStaticPropsContext
- options:
- prefix: path prefix
- params: JSON API params for filtering, includes, sorting..etc
- deserialize: set to
true
if the return data should be deserialize
Example:
export async function getStaticProps(context) {
let articles = await getEntitiesFromContext("node", "article", context, {
params: {
include: "field_image, uid",
sort: "-created",
},
})
return {
props: {
articles,
},
revalidate: 1,
}
}
- entity_type: the id of the entity_type
- bundle: the bundle for the entity
- context: GetStaticPropsContext
- options:
- prefix: path prefix
- params: JSON API params for filtering, includes, sorting..etc
- deserialize: set to
true
if the return data should be deserialize
Example:
export async function getStaticProps(context) {
const post = await getEntityFromContext("node", "article", context, {
prefix: "/blog",
params: {
include: "field_image, uid",
},
})
if (!post) {
return {
notFound: true,
}
}
return {
props: {
post,
},
revalidate: 1,
}
}
To create preview routes for an entity type on your Next.js site:
- First, copy environment variables in your .env.local file:
You can grab the environment variables for a site by visiting the Environment variables page in Drupal (see /admin/config/services/next).
NEXT_PUBLIC_DRUPAL_BASE_URL=
NEXT_IMAGE_DOMAIN=
DRUPAL_SITE_ID=
DRUPAL_CLIENT_ID=
DRUPAL_CLIENT_SECRET=
DRUPAL_PREVIEW_SECRET=
- Next, create a page with a dynamic route:
pages/blog/[...slug].jsx
// pages/blog/[...slug].jsx
import { getPathsForEntityType, getEntityFromContext } from "next-drupal"
export default function BlogPostPage({ post }) {
if (!post) return null
return (
<article>
<h1>{post.title}</h1>
{post.body?.processed && (
<div dangerouslySetInnerHTML={{ __html: post.body?.processed }} />
)}
</article>
)
}
export async function getStaticPaths() {
const paths = await getPathsForEntityType("node", "article", {
params: {
"filter[status]": 1,
},
})
return {
paths,
fallback: true,
}
}
export async function getStaticProps(context) {
const post = await getEntityFromContext("node", "article", context, {
prefix: "/blog",
params: {
include: "field_image, uid",
},
})
if (!post) {
return {
notFound: true,
}
}
return {
props: {
post,
},
revalidate: 1,
}
}
- Create a
/api/preview
route:pages/api/preview.js
// pages/api/preview.js
import { NextApiRequest, NextApiResponse } from "next"
export default function (request: NextApiRequest, response: NextApiResponse) {
const { slug, resourceVersion, secret } = request.query
if (secret !== process.env.DRUPAL_PREVIEW_SECRET) {
return response.status(401).json({ message: "Invalid preview secret." })
}
if (!slug) {
return response.status(401).json({ message: "Invalid slug." })
}
response.setPreviewData({
resourceVersion,
})
response.redirect(slug as string)
}
- Create a
/api/exit-preview
route:pages/api/exit-preview.js
.
// pages/api/exit-preview.js
import { NextApiResponse } from "next"
export default async function exit(_, response: NextApiResponse) {
response.clearPreviewData()
response.writeHead(307, { Location: "/" })
response.end()
}
That's it. You should now be able to preview entities from within your Drupal site.
Chapter Three: Development and support