Let Editors Build Custom Pages in Next.js with Dyrected
Set up Dyrected so your marketing & content team or clients can build new landing pages from approved reusable sections without waiting on a developer for every copy and layout change.
Use this page after Adding a Visual Editor in Next.js. This is the page where schema, rendering, and visual preview finally become one complete workflow for editors. At this point, the important pieces should already exist:
- the
pagescollection exists - the frontend can render Dyrected content
- preview can open the real route and reflect edits
Now the goal changes. Instead of proving the integration works, you are deciding whether the setup is ready for editors to build real campaign and landing pages without developer help for every change.
By the end, you should know what a safe custom-page workflow looks like, which parts must stay controlled by code, and how the schema, rendering, and preview pieces fit together into one editor-ready system.
Before this page:
- editors can preview and click into content on a real route
- the frontend can render Dyrected content correctly
After this page:
- editors can assemble a new landing page from approved sections
- preview still shows the real route while they work
- the frontend keeps the design system and layout guardrails
This guide assumes you already have some real landing-page sections in your frontend. The missing job is turning those existing sections into reusable Dyrected blocks one by one.
Start with the right mental model
For custom landing pages, the goal is usually not "let editors design anything." The goal is "let editors assemble approved page sections fast."
That means the safest setup is:
- the frontend keeps the design system
- the schema defines the approved section types
- editors choose the order and content inside those sections
In Dyrected, that setup is usually:
- a
pagescollection with alayoutblocks field - a Next.js renderer that maps each
blockTypeto a real component - a preview setup that opens the real page while editors are working
Choose the sections editors should actually control
Before you write any code, list the section types the team needs most often.
A strong first set is usually small:
- hero
- feature grid
- testimonial
- call to action
If a section is not already designed in the frontend, do not add it yet. The fastest dashboard is the one that only offers sections the site already knows how to render well.
Start from existing frontend components
Do not design the block schema in a vacuum. Pick one existing section from the site and treat that component as the source of truth for the first block.
For each section you want editors to use:
- find the real React component that already renders that section
- list the props or content inputs it already expects
- separate true content fields from styling and layout internals
- keep only the content fields editors should control
For example, an existing hero section might already look roughly like this:
type HeroSectionProps = {
heading: string;
subheading?: string;
ctaLabel?: string;
ctaUrl?: string;
image?: Media;
};
export function HeroSection({
heading,
subheading,
ctaLabel,
ctaUrl,
image,
}: HeroSectionProps) {
return (
<section>
<h1>{heading}</h1>
{subheading ? <p>{subheading}</p> : null}
{ctaLabel && ctaUrl ? <a href={ctaUrl}>{ctaLabel}</a> : null}
{image ? <DyrectedImage media={image} alt={heading} /> : null}
</section>
);
}That is a good first block candidate because the editable inputs are already clear. The component already owns the layout and styling, so the block only needs to expose the content fields.
Turn one component into one reusable block
Once you know which existing section you are modeling, create a block that matches the content inputs that component really needs.
The safest quick-start rule is:
- one visible section type equals one block
- one block should map cleanly to one existing frontend component
- block fields should describe content, not implementation details
For the hero example above, the block can look like this:
const HeroBlock = defineBlock({
slug: "hero",
labels: { singular: "Hero", plural: "Heroes" },
fields: [
defineTextField({ name: "heading", label: "Heading", required: true }),
defineTextareaField({ name: "subheading", label: "Subheading" }),
defineTextField({ name: "ctaLabel", label: "CTA label" }),
defineUrlField({ name: "ctaUrl", label: "CTA URL" }),
defineRelationshipField({
name: "image",
label: "Image",
relationTo: "media",
}),
],
});That mapping should feel boring on purpose. If the component already works, the block should usually mirror its content inputs instead of inventing a new API.
Add a layout blocks field to the pages collection
After you have converted the first few existing sections into blocks, add them to a layout field on the pages collection. This is the key schema change that turns a basic page model into a controlled landing-page builder.
import {
defineBlock,
defineBlocksField,
defineCollection,
defineRelationshipField,
defineRichTextField,
defineTextField,
defineTextareaField,
defineUrlField,
} from "@dyrected/core";
const HeroBlock = defineBlock({
slug: "hero",
labels: { singular: "Hero", plural: "Heroes" },
fields: [
defineTextField({ name: "heading", label: "Heading", required: true }),
defineTextareaField({ name: "subheading", label: "Subheading" }),
defineTextField({ name: "ctaLabel", label: "CTA label" }),
defineUrlField({ name: "ctaUrl", label: "CTA URL" }),
defineRelationshipField({
name: "image",
label: "Image",
relationTo: "media",
}),
],
});
const FeatureGridBlock = defineBlock({
slug: "featureGrid",
labels: { singular: "Feature grid", plural: "Feature grids" },
fields: [
defineTextField({ name: "heading", label: "Heading", required: true }),
defineRichTextField({ name: "intro", label: "Intro" }),
],
});
const TestimonialBlock = defineBlock({
slug: "testimonial",
labels: { singular: "Testimonial", plural: "Testimonials" },
fields: [
defineTextField({ name: "quote", label: "Quote", required: true }),
defineTextField({ name: "name", label: "Name", required: true }),
defineTextField({ name: "role", label: "Role" }),
],
});
const CallToActionBlock = defineBlock({
slug: "callToAction",
labels: { singular: "Call to action", plural: "Calls to action" },
fields: [
defineTextField({ name: "heading", label: "Heading", required: true }),
defineTextareaField({ name: "body", label: "Body" }),
defineTextField({ name: "label", label: "Button label", required: true }),
defineUrlField({ name: "url", label: "Button URL", required: true }),
],
});
export const Pages = defineCollection({
slug: "pages",
labels: { singular: "Page", plural: "Pages" },
admin: {
useAsTitle: "title",
previewUrl: "slug == 'home' ? '/' : '/' + slug",
urlPattern: "/{slug}",
},
fields: [
defineTextField({ name: "title", label: "Title", required: true }),
defineTextField({ name: "slug", label: "Slug", required: true }),
defineBlocksField({
name: "layout",
label: "Page layout",
blocks: [HeroBlock, FeatureGridBlock, TestimonialBlock, CallToActionBlock],
}),
],
});This keeps the page structure flexible, but only inside the set of blocks you approved.
Make sure the frontend can render every approved section
The dashboard is only half of the workflow. Every approved block also needs a matching Next.js component in the frontend.
That means:
- every
blockTypein the schema must map to a real component - the page route must render the
layoutfield in order - preview must open the same route editors are trying to build
If one of those pieces is missing, this stops being a custom-page workflow and turns back into a schema exercise.
In practice, the simplest path is usually to keep your existing section component and add a thin adapter only if the prop shape needs cleanup.
For example, a route can render the approved blocks like this:
import { Blocks } from "@dyrected/next";
import { HeroSection } from "@/components/marketing/HeroSection";
import { FeatureGridSection } from "@/components/marketing/FeatureGridSection";
import { TestimonialSection } from "@/components/marketing/TestimonialSection";
import { CallToActionSection } from "@/components/marketing/CallToActionSection";
function HeroBlockView(props: {
heading: string;
subheading?: string;
ctaLabel?: string;
ctaUrl?: string;
image?: Media;
}) {
return <HeroSection {...props} />;
}
<Blocks
items={page.layout}
path="layout"
components={{
hero: HeroBlockView,
featureGrid: FeatureGridSection,
testimonial: TestimonialSection,
callToAction: CallToActionSection,
}}
/>;If you need the shared rendering pattern first, go back to Displaying Content in Next.js, especially the section on rendering blocks.
Repeat that conversion for each approved section
At this point, the workflow for every new landing-page section is the same:
- start from an existing frontend component
- identify the content inputs editors should control
- define a matching Dyrected block
- add that block to the
layoutfield - register the matching component in the frontend
Blocksmapping - confirm the section renders and previews correctly
That repetition is normal. A landing-page builder is not one magic switch. It is a small library of approved section components that you wire into the schema and renderer one by one.
Keep the page builder controlled
This is where the setup either stays useful or becomes messy.
For a quick-start editorial workflow:
- keep global site chrome like navigation and settings outside the page layout
- keep one block focused on one visible section type
- keep field names content-focused, not implementation-focused
- avoid adding arbitrary JSON fields or open-ended style controls
A good page builder helps marketing move fast inside guardrails. It should not ask them to invent layout rules the frontend does not already understand.
Build one real landing page in the dashboard
Once schema, rendering, and preview all exist, create one real landing page in the admin and add a few sections in order:
- a hero
- a feature grid
- a testimonial
- a call to action
Then open that page in preview and make one real editorial change: update the hero copy, reorder a section, or replace a call to action.
That is the moment this guide is aiming for. It proves editors are no longer just editing fields. They are building a real page inside the system you prepared for them.
Know what this page depends on
This workflow only works because the earlier quick-start pages already handled the pieces below:
- Defining a Schema shaped the content model
- Setting Up Initial Data made the first load non-empty
- Displaying Content in Next.js made the frontend render real content
- Adding a Visual Editor in Next.js made editors preview and click into that content
This page is where those pieces finally become one editor-ready landing-page workflow.
What this setup gives editors
Once this is in place, the dashboard can support the tasks marketing teams usually care about most:
- create a new landing page
- reorder approved page sections
- update hero copy, proof, and calls to action
- publish a campaign page without asking a developer for every text edit
What it does not do is allow arbitrary new section types or arbitrary design changes. That stays with the product or frontend team, which is usually the safer split.
Where to go deeper
Use this page for the quick-start version of the pattern. For the full background, continue to:
Success check
You are ready for the next step when:
- the
pagescollection has alayoutblocks field - the allowed block types match real designed page sections
- the frontend renders those sections correctly
- preview reflects changes on a real page
- the dashboard can create at least one landing page from those approved sections
Once that is true, editors can create a page, add approved sections, reorder them, preview the result on the real route, and publish routine content changes without developer help. When you are ready to give access to another person, continue to Handing Off to Editors.