You're reading docs for Nextly Alpha. APIs may change between releases.

Page Builder

Page Builder

Let your team compose pages from blocks you define, rendered on the server with no client JavaScript by default.

The Page Builder gives non-developers a canvas. They drag blocks onto a page, arrange them, set text and images, and publish. You decide which blocks exist.

That split is the point. Your developers decide what is possible; your team decides what goes on the page. Someone editing a page cannot invent a new kind of section, so a site cannot drift away from its design system by accident.

It ships as a plugin, @nextlyhq/plugin-page-builder, built on the same public API any plugin uses.

What you get

  • 20 blocks out of the box — layout (section, box, columns, column, card, accordion, accordion item, spacer, divider), content (heading, paragraph, rich text, list, quote, image, gallery, button, embed, form), and one that reads from your content (collection loop).
  • Your own blocks, defined in code, sitting beside the built-in ones.
  • Responsive control per block, against breakpoints you declare.
  • Server-first rendering. A page ships no client JavaScript unless a block on it needs some.

Availability

Package@nextlyhq/plugin-page-builder, plus @nextlyhq/blocks-react in your app
StatusAlpha. The block model, registries and render pipeline are stable; editor interactions are still evolving
PermissionEditing a page requires update access to the collection holding it

Install

pnpm add @nextlyhq/plugin-page-builder @nextlyhq/blocks-react \
  @nextlyhq/builder @nextlyhq/plugin-sdk react-hook-form

Add the plugin to your config. It contributes a pages collection whose edit screen is the builder:

import { pageBuilder } from "@nextlyhq/plugin-page-builder";
import { defineConfig } from "nextly";

export default defineConfig({
  plugins: [pageBuilder()],
});

Render the pages

A plugin cannot add routes to your Next.js app, so you declare one catch-all route and let createBlocksPage fill it. It resolves the incoming path against your collections, 404s on a miss, and renders the stored document.

// app/(frontend)/[...slug]/page.tsx
import { createBlockResolver } from "@nextlyhq/blocks-react";
import { coreBlocks } from "@nextlyhq/blocks-react/blocks";
import { createBlocksPage } from "@nextlyhq/blocks-react/next";
import { getNextly } from "nextly";
import type { NextlyContentReader } from "nextly/runtime";

import nextlyConfig from "../../../nextly.config";

type NextlyInstance = Awaited<ReturnType<typeof getNextly>>;

const instance = () => getNextly({ config: nextlyConfig });

// Annotated rather than inferred: without a contextual type these `args`
// parameters are implicitly `any`, which fails `next build` under strict mode.
const reader: NextlyContentReader & {
  media: Pick<NextlyInstance["media"], "findByID">;
} = {
  find: async args => (await instance()).find(args),
  findByID: async args => (await instance()).findByID(args),
  // Images that store a media id resolve through this. Omit it and those images
  // render nothing, while images holding a literal URL keep working.
  media: {
    findByID: async args => (await instance()).media.findByID(args),
  },
};

const { ContentPage, generateMetadata } = createBlocksPage({
  collections: ["pages"],
  field: "content",
  nextly: reader,
  blocks: createBlockResolver(coreBlocks),
  styleContext: {
    breakpoints: {
      viewport: [
        { id: "base", label: "Base" },
        { id: "tablet", label: "Tablet", maxWidth: 1024 },
        { id: "mobile", label: "Mobile", maxWidth: 640 },
      ],
      container: [],
    },
  },
});

export { generateMetadata };
export default ContentPage;

Without a breakpoint set the renderer emits class names with no CSS behind them, and pages come out structurally correct and visually bare. Declare at least a base breakpoint.

Public sites read differently

createBlocksPage reads access-enforced content, so it needs no database during next build. A wholly public site calls createPublicBlocksPage instead: it reads trusted, pre-renders, and returns a generateStaticParams to export alongside the other two. Which one you call is the security posture — there is no option to flip.

Use it as a field instead

You do not have to accept the pages collection. The builder is also a field, usable in any collection or single next to ordinary fields:

import { blocks } from "@nextlyhq/plugin-page-builder";
import { defineSingle, text } from "nextly/config";

export const Homepage = defineSingle({
  slug: "homepage",
  fields: [text({ name: "title" }), blocks({ name: "content" })],
});

Reading content into a page

The collection loop block queries one of your collections and repeats its child blocks once per entry — a post list, a team grid, a product row — without anyone writing a query. The developer decides which collections are available to it; the editor decides what each repeated item looks like.