Next.js Tutorial
This guide will walk you through setting up Composify in your Next.js project. We'll assume you already have a Next.js project up and running. If not, start with the Next.js getting started guide first.
First, make sure the mock API from our prerequisites guide is running on http://localhost:9000
. We'll use it to read and write our page content.
Install Composify
Add Composify to your project with your preferred package manager:
npm install @composify/react --save
Register your components
You can use plain HTML elements, but Composify really shines when you use your own components. Let's create a few simple ones.
/* components/Heading.tsx */
import { createElement, type FC, type PropsWithChildren } from 'react';
type Props = PropsWithChildren<{
level?: 1 | 2 | 3;
weight?: 'semibold' | 'bold' | 'extrabold';
}>;
const TEXT_SIZE = {
1: 'text-4xl',
2: 'text-3xl',
3: 'text-2xl',
};
const FONT_WEIGHT = {
semibold: 'font-semibold',
bold: 'font-bold',
extrabold: 'font-extrabold',
};
export const Heading: FC<Props> = ({
level = 1,
weight = 'extrabold',
children,
}) =>
createElement(
`h${level}`,
{ className: `text-neutral-900 ${FONT_WEIGHT[weight]} ${TEXT_SIZE[level]}` },
children
);
Now, let's register them in the Catalog:
/* components/Heading.tsx */
import { Catalog } from '@composify/react/renderer';
/* ... */
Catalog.register('Heading', {
component: Heading,
props: {
level: {
label: 'Heading Level',
type: 'radio',
options: [
{
label: '1',
value: 1,
},
{
label: '2',
value: 2,
},
{
label: '3',
value: 3,
},
],
default: 1,
},
weight: {
label: 'Font Weight',
type: 'select',
options: [
{
label: 'Semibold',
value: 'semibold',
},
{
label: 'Bold',
value: 'bold',
},
{
label: 'Extrabold',
value: 'extrabold',
},
],
default: 'extrabold',
},
children: {
label: 'Content',
type: 'text',
default: 'Server Driven UI made easy',
},
},
});
Finally, create a central export file at components/index.ts
so we can import them all with a single line:
export { Heading } from './Heading';
export { Body } from './Body';
export { Button } from './Button';
export { HStack } from './HStack';
export { VStack } from './VStack';
Render a page
With our components registered, let's render a page. The Renderer
takes the saved JSX and renders it using your components.
import '@/components';
import { Renderer } from '@composify/react/renderer';
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const res = await fetch(`http://localhost:9000/documents/${slug}`, {
cache: 'no-store',
});
const { content } = await res.json().catch(() => ({}));
if (!content) {
return notFound();
}
return (
<Renderer source={content} />
);
}
Now you can test it:
- Visit
http://localhost:3000/foo/
to see the saved page. - Visit
http://localhost:3000/baz/
and you'll get a 404 because there's no data yet.
Set up the Editor
Now for the fun part: setting up the visual editor. To create or update content, we'll use the Editor
component. Note that the editor is interactive, so it must be in a Client Component ('use client'
).
'use client';
import '@composify/react/style.css';
import '@/components';
import { Editor } from '@composify/react/editor';
export default function EditorPage({ slug, content }: { slug: string; content: string }) {
return <Editor title={slug} source={content} />;
}
A few key points:
@composify/react/style.css
is required — it contains the core editor styles.- Import your components (
@/components
) so they're available in the editor.
Load the initial source
First, we'll fetch the saved JSX from our API and pass it to the Editor
as the source
prop.
import EditorPage from './client';
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const res = await fetch(`http://localhost:9000/documents/${slug}`, {
cache: 'no-store',
});
const { content } = await res.json().catch(() => ({}));
return <EditorPage slug={slug} content={content ?? '<VStack size={{ height: 100 }} backgroundColor="#f8fafc" />'} />;
}
Open http://localhost:3000/editor/foo/
and you should see the editor UI with the document loaded.
Handle saving
Right now, clicking Save does nothing. Let's wire it up to our API using the onSubmit
handler.
'use client';
import '@composify/react/style.css';
import '@/components';
import { Editor } from '@composify/react/editor';
import { useRouter } from 'next/navigation';
export default function EditorPage({ slug, content }: { slug: string; content: string }) {
const router = useRouter();
const handleSubmit = async (source: string) => {
await fetch(`http://localhost:9000/documents/${slug}`, {
method: 'DELETE',
}).catch(() => null);
await fetch('http://localhost:9000/documents', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: slug,
content: source,
}),
});
if (!window.confirm('Saved successfully. Keep editing?')) {
router.push(slug);
}
};
return <Editor title={slug} source={content} onSubmit={handleSubmit} />;
}
Now, when you hit Save, the editor sends the updated JSX to your /documents
API.
If you select No in the confirmation dialog, you'll be redirected to the rendered page.
Try it out!
- Visit
http://localhost:3000/foo/
to see the saved content. - Open
http://localhost:3000/editor/foo/
, make a change, and click Save — the rendered page updates instantly. - Visit
http://localhost:3000/baz/
to see a 404. - Open
http://localhost:3000/editor/baz/
, creat content, click Save, and the new page will be live immediately 🎉
Wrapping up
And that's it! You now have:
- A document store (currently powered by json-server, but could be a real database later)
- An editor where you can visually compose pages using your own components
- A renderer that turns saved JSX back into real UI
Where to go from here?
- Replace json-server with a real database
- Add authentication and user permissions
- Deploy it so your whole team can collaborate
For unlimited bandwidth, built-in version history, and collaboration features, try Composify Cloud — or self-host it, since it's all open source.