The future of Drupal is headless

Next.js for Drupal has everything you need to build a next-generation front-end for your Drupal site.

Everything you expect from Drupal.
On a modern stack.

Go headless without compromising features.

Seamless Editing

Inline preview built-in to the editing interface.

Instant Publishing

New content and updates are live instantly.

Multi-site

Power multiple Next.js sites from one Drupal site.

Authentication

Authentication with support for roles and permissions.

Webforms

Built React forms backed by the Webform module.

Search API

Support for decoupled faceted search powered by Search API.

Internationalization

Built-in translation and Automatic Language detection.

Performance

Deploy and scale your sites via content delivery networks.

Security

Protect your site from attacks by separating code from the interface.

Out-of-the-box tooling for the best developer experience

Build all the features you need. Faster.

import { getPathsFromContext, getResourceFromContext } from "next-drupal"

export default function ArticleNodePage({ node }) {
  return node ? (
    <div>
      <h1>{node.title}</h1>
    </div>
  ) : null
}

export async function getStaticPaths(context) {
  return {
    paths: await getPathsFromContext("node--article", context),
    fallback: false,
  }
}

export async function getStaticProps(context) {
  const node = await getResourceFromContext("node--article", context)

  return {
    props: {
      node,
    },
  }
}
import { getMenu } from "next-drupal"

export async function getStaticProps() {
  // Fetch the main menu.
  return {
    props: {
      menu: await getMenu("main"),
    },
  }
}
import Link from "next/link"
import { useMenu } from "next-drupal"

export default function Page({ node }) {
  const { tree } = useMenu("main")

  return (
    <ul>
      {tree?.map((item) => (
        <li key={item.id}>
          <Link href={item.url} passHref>
            <a>{item.title}</a>
          </Link>
        </li>
      ))}
    </ul>
  )
}
import { getResource } from "next-drupal"

// Get the `es` translation for a page by uuid.
const node = await getResource(
  "node--page",
  "07464e9f-9221-4a4f-b7f2-01389408e6c8",
  {
    locale: "es",
    defaultLocale: "en",
  }
)
import { getMenu } from "next-drupal"

export async function getStaticProps(context) {
  // Get translated menu from context.
  const menu = await getMenu("main", context)

  return {
    props: {
      menu,
    },
  }
}
import NextAuth from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"

export default NextAuth({
  providers: [
    CredentialsProvider({
      name: "Credentials",
      async authorize(credentials) {
        const formData = new URLSearchParams()
        formData.append("grant_type", "password")
        formData.append("username", credentials.username)
        formData.append("password", credentials.password)

        // Get access token from Drupal.
        const response = await fetch(
          `${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}/oauth/token`,
          {
            method: "POST",
            body: formData,
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
          }
        )

        const data = await response.json()

        if (response.ok && data?.access_token) {
          return data
        }

        return null
      },
    }),
  ],
})
export default async function handler(request, response) {
  try {
    await fetch(
      `${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}/webform_rest/submit`,
      {
        method: "POST",
        body: JSON.stringify({
          webform_id: request.query.form,
          name: request.body.name,
        }),
      }
    )

    response.status(200).end()
  } catch (error) {
    return response.status(400).json(error.message)
  }
}
export default function ContactPage() {
  async function handleSubmit() {
    await fetch(`/api/forms/contact`, {
      method: "POST",
      body: JSON.stringify({
        name: event.target.name.value,
      }),
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor="name">Name</label>
      <input type="text" id="name" name="name" />
      <button type="submit">Submit</button>
    </form>
  )
}
import { getSearchIndex } from "next-drupal"

export default async function handler(request, response) {
  try {
    const body = JSON.parse(request.body)

    const results = await getSearchIndex(request.query.index, body)

    response.json(results)
  } catch (error) {
    return response.status(400).json(error.message)
  }
}
export default function SearchPage() {
  const [results, setResults] = React.useState<>([])

  async function handleSubmit() {
    const response = await fetch("/api/search/articles", {
      method: "POST",
      body: JSON.stringify({
        params: {
          filter: {
            fulltext: event.target.keywords.value,
          },
        },
      }),
    })

    const results = await response.json()

    setResults(results)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="search" name="keywords" />
      <button type="submit">Search</button>
    </form>
  )
}
import {
  getPathsFromContext,
  getResourceCollectionFromContext,
  DrupalNode,
} from "next-drupal"

interface BlogPageProps {
  nodes: DrupalNode[]
}

export default function BlogPage({ nodes }: BlogPageProps) {
  return ...
}

export async function getStaticProps(
  context
): Promise<GetStaticPropsResult<BlogPageProps>> {
  const nodes = await getResourceCollectionFromContext<DrupalNode[]>(
    "node--article",
    context
  )

  return {
    props: {
      nodes,
    },
  }
}