はじめに
正直なところ、「ページビルダーを自社で作りたい」って思ったことありませんか。
WordPressのElementorやWixのエディタみたいな、ドラッグ&ドロップでページを作れるやつ。クライアントから「コードを書かずにページを編集したい」と言われて、既存のノーコードツールを導入するか悩んだ経験のある人も多いはず。
でも既存ツールって、カスタマイズ性に限界があるんですよね。自社のデザインシステムに合わせたい、独自のコンポーネントを使いたい...そんなときに困る。
そこで登場するのがPuckです。
PuckはGitHubで10,200スター以上を獲得している、React向けのオープンソースビジュアルエディタ。自分で定義したReactコンポーネントを、ユーザーがドラッグ&ドロップで配置できるページビルダーが作れます。
これ、意外と革命的なんですよ。
とはいえ、説明を読むより先に触ってみた方が早いと思います。ドラッグ&ドロップでブロックを組み立てられるPuckのエディタを、記事内でそのまま動かせるサンプルを用意しました。先に触りたい方はこちらからどうぞ。
Puckとは
Puckは、React.js向けのモジュール型ビジュアルエディタです。公式では「The visual editor for React」と謳っています。
最大の特徴は自分のReactコンポーネントをそのまま使えること。他のノーコードツールと違って、既存のデザインシステムやUIライブラリをそのままページビルダーに組み込めます。
// 自分のコンポーネントをそのまま登録
const config = {
components: {
Hero: {
render: ({ title, subtitle }) => (
<div className="hero">
<h1>{title}</h1>
<p>{subtitle}</p>
</div>
),
fields: {
title: { type: "text" },
subtitle: { type: "text" },
},
},
},
};
これだけで、ユーザーがドラッグ&ドロップでHeroコンポーネントを配置し、タイトルやサブタイトルを編集できるようになります。
公式サイト: https://puckeditor.com GitHub: https://github.com/measuredco/puck
特徴・メリット
1. 完全なデータ所有権
PuckはSaaSではなく、オープンソースのライブラリです。編集されたページのデータは完全に自分のものになります。
{
"content": [
{
"type": "Hero",
"props": {
"title": "ようこそ",
"subtitle": "私たちのサービスへ"
}
}
],
"root": {}
}
JSONで出力されるので、どこにでも保存できる。データベースでもファイルでも、S3でも好きなところに。ベンダーロックインを気にする必要がないのは、30代エンジニアとしてはかなり安心感がありますね。
2. Reactエコシステムとの親和性
PuckはReactコンポーネントとして動作します。Next.js、Remix、React Routerなど、好きなフレームワークと組み合わせ可能。
公式で以下のレシピが提供されています:
- Next.js (App Router対応)
- Remix Run v2
- React Router v7
個人的には、Next.jsのApp Routerと組み合わせるのが一番使いやすいと思います。
3. MITライセンス
商用利用も無料です。社内ツールでも、クライアントに納品するシステムでも、ライセンス料を気にせず使えます。
コスパ的に最高ですね。
4. 軽量で依存関係が少ない
パッケージサイズは約98.6kB(gzip圧縮時)。ビジュアルエディタとしては十分軽量です。
重たいエディタを入れたらサイトが遅くなった...という心配は少ない。
5. AI機能のサポート
最新のPuck AI 0.3では、ヘッドレスなAI生成APIが利用可能になりました。AIを使ったページ生成やコンテンツ提案など、先進的な機能も視野に入れた開発が進んでいます。
インストール方法
新規プロジェクトを始める場合
npx create-puck-app my-app
これで、Puckを使ったプロジェクトのひな形が生成されます。時短になりますね。
既存プロジェクトに追加する場合
npm install @measured/puck
または
yarn add @measured/puck
Puckのサンプルを動かす
Puckの核になるのは<Puck>コンポーネントです。コンポーネントの定義を集めたconfig.componentsと、編集対象のデータ(data)を渡すだけで、ドラッグ&ドロップでブロックを配置・編集できるエディタ画面がまるごと出来上がります。
要点だけを抜き出すと、こんな形です。fieldsでエディタ側の入力UIを、renderで実際の見た目を定義します。
const config = {
components: {
HeadingBlock: {
fields: { text: { type: "text" } },
defaultProps: { text: "見出し" },
render: ({ text }) => <h2>{text}</h2>,
},
TextBlock: {
fields: { content: { type: "textarea" } },
defaultProps: { content: "本文" },
render: ({ content }) => <p>{content}</p>,
},
},
};
<Puck
config={config}
data={{ content: [], root: {} }}
onPublish={(data) => console.log(data)}
/>;
実際に動かせるものが下です。左側のコンポーネント一覧から「HeadingBlock」や「TextBlock」を中央のキャンバスにドラッグ&ドロップし、右側のパネルでテキストを書き換えてみてください。画面右上の「Publish」を押すと、onPublishに渡されたdata(配置したブロックとpropsの一覧)がコンソールに出力されます。
config.componentsに新しいキーを追加すれば、そのままコンポーネント一覧に選択肢が増えます。たとえばfieldsにtype: "select"のフィールドを足すと、色やサイズなどをドラッグ&ドロップ後にパネルから選べるようになります。もっと複数のコンポーネントを組み合わせたレイアウト例と、<Render>コンポーネントで編集結果だけを表示する例は、「実践的なユースケース」で紹介します。
基本的な使い方
エディタを表示する
まず、コンポーネントの設定を定義し、<Puck>コンポーネントをレンダリングします。
import { Puck } from "@measured/puck";
import "@measured/puck/puck.css";
// コンポーネントの設定
const config = {
components: {
HeadingBlock: {
fields: {
text: { type: "text" },
level: {
type: "select",
options: [
{ label: "H1", value: "h1" },
{ label: "H2", value: "h2" },
{ label: "H3", value: "h3" },
],
},
},
defaultProps: {
text: "見出し",
level: "h1",
},
render: ({ text, level }) => {
const Tag = level;
return <Tag>{text}</Tag>;
},
},
TextBlock: {
fields: {
content: { type: "textarea" },
},
defaultProps: {
content: "本文テキスト",
},
render: ({ content }) => <p>{content}</p>,
},
},
};
// 初期データ
const initialData = {
content: [],
root: {},
};
// 保存時の処理
const handlePublish = (data) => {
console.log("保存されたデータ:", data);
// ここでAPIに送信するなど
};
export default function Editor() {
return (
<Puck config={config} data={initialData} onPublish={handlePublish} />
);
}
これだけで、ドラッグ&ドロップでHeadingBlockとTextBlockを配置できるエディタが完成します。
保存したページを表示する
編集したページを公開側で表示するには、<Render>コンポーネントを使います。
import { Render } from "@measured/puck";
export default function Page({ data }) {
return <Render config={config} data={data} />;
}
エディタとビューアを分離できるので、編集権限のあるユーザーだけにエディタを見せる、という実装も簡単です。
Next.js App Routerでの使用例
// app/editor/page.tsx
"use client";
import { Puck } from "@measured/puck";
import "@measured/puck/puck.css";
import { config } from "@/lib/puck-config";
export default function EditorPage() {
const handlePublish = async (data) => {
await fetch("/api/pages", {
method: "POST",
body: JSON.stringify(data),
});
};
return (
<Puck
config={config}
data={{ content: [], root: {} }}
onPublish={handlePublish}
/>
);
}
// app/[slug]/page.tsx
import { Render } from "@measured/puck";
import { config } from "@/lib/puck-config";
import { getPageData } from "@/lib/api";
export default async function Page({ params }) {
const data = await getPageData(params.slug);
return <Render config={config} data={data} />;
}
App Routerを使っている場合、エディタページは"use client"が必要なので注意。
実践的なユースケース
1. 社内向けCMSの構築
マーケティングチームが自分でランディングページを作りたい。でも外部のノーコードツールは社内ルール的に使えない...そんなときにPuckが活躍します。
自社のデザインシステムに沿ったコンポーネントだけを提供すれば、ブランドの一貫性を保ちながら非エンジニアでもページが作れます。
2. クライアント向けページビルダー
Web制作会社なら、納品後にクライアント自身がページを編集できる仕組みを提供できます。WordPressよりも軽量で、カスタマイズ性も高い。
3. メールテンプレートエディタ
Reactでメールテンプレートを作っているなら、Puckで編集UIを提供できます。react-emailなどと組み合わせると便利。
4. ダッシュボードのカスタマイズ
ユーザーが自分でダッシュボードのレイアウトをカスタマイズできるようにする。ウィジェットをドラッグ&ドロップで配置させるUIにPuckを使えます。
5. ドキュメント・ブログエディタ
Notion風のブロックエディタも、Puckをベースに構築可能。ブロックタイプを定義すれば、リッチなコンテンツエディタが作れます。
6. Puckで複数コンポーネントを定義してレイアウトを組む
見出しやテキストだけでなく、カードやボタンなど複数の種類のコンポーネントをconfig.componentsに登録しておくと、それらを自由に組み合わせてページ全体のレイアウトを作れます。ランディングページのセクションを、あらかじめ用意したブロックの組み合わせだけで非エンジニアに構築させたいときに使えるパターンです。
selectタイプのfieldsを使うと、色やバリエーションをパネルから選べるようになります。
const config = {
components: {
CardBlock: {
fields: {
title: { type: "text" },
body: { type: "textarea" },
},
render: ({ title, body }) => (
<div className="card">
<h3>{title}</h3>
<p>{body}</p>
</div>
),
},
ButtonBlock: {
fields: {
label: { type: "text" },
color: {
type: "select",
options: [
{ label: "青", value: "#2563eb" },
{ label: "緑", value: "#16a34a" },
],
},
},
render: ({ label, color }) => (
<button style={{ background: color }}>{label}</button>
),
},
},
};
実際に動かせるサンプルです。「HeadingBlock」「CardBlock」「ButtonBlock」の3種類をキャンバスに配置し、ButtonBlockのパネルで色を切り替えてみてください。複数のコンポーネントが同じキャンバス上でどう積み重なるかが確認できます。
config.componentsにキーを追加するだけで、ドラッグ&ドロップの候補は際限なく増やせます。自社のデザインシステムのコンポーネントをそのまま登録すれば、ブランドに沿ったページだけを組み立てさせることも可能です。
7. Puckの<Render>コンポーネントで編集結果を独立して表示する
編集中は<Puck>、公開後は<Render>という使い分けをすると、編集用UIと閲覧用の表示を完全に分離できます。同じconfigとdataを渡すだけで、<Render>側にはドラッグ&ドロップのUIを含まない、素の表示結果だけが描画されます。編集権限のないユーザーには<Render>だけを見せる、という実装がそのまま作れます。
import { Puck, Render } from "@measured/puck";
function App() {
const [data, setData] = useState(initialData);
const [mode, setMode] = useState("edit");
if (mode === "view") {
return <Render config={config} data={data} />;
}
return (
<Puck
config={config}
data={data}
onPublish={(next) => {
setData(next);
setMode("view");
}}
/>
);
}
実際に動かせるサンプルです。ブロックを編集してから画面右上の「Publish」を押すと、onPublishで受け取ったデータを使って<Render>側の表示に切り替わります。「編集画面に戻る」ボタンで、いつでも<Puck>の編集モードに戻れます。
<Puck>と<Render>が同じconfigを参照しているため、エディタ側で見た目を変えれば表示側にもそのまま反映されます。公開ページ側は<Render>だけで組み立てておけば、ドラッグ&ドロップ用の編集UIを混在させる必要がありません。
高度な使い方
カスタムフィールドの定義
標準のtext、textareaの他に、カスタムフィールドも作れます。
const config = {
components: {
ImageBlock: {
fields: {
src: {
type: "custom",
render: ({ value, onChange }) => (
<div>
<input
type="text"
value={value || ""}
onChange={(e) => onChange(e.target.value)}
placeholder="画像URL"
/>
{value && <img src={value} alt="" width={100} />}
</div>
),
},
alt: { type: "text" },
},
render: ({ src, alt }) => (
<img src={src} alt={alt} className="w-full" />
),
},
},
};
画像アップロードUIを独自に実装したい場合などに使えます。
ネストされたコンポーネント
コンポーネントの中に別のコンポーネントを配置することもできます。
const config = {
components: {
Container: {
render: ({ puck: { renderDropZone } }) => (
<div className="container">
{renderDropZone({ zone: "content" })}
</div>
),
},
Card: {
fields: {
title: { type: "text" },
},
render: ({ title, puck: { renderDropZone } }) => (
<div className="card">
<h3>{title}</h3>
{renderDropZone({ zone: "body" })}
</div>
),
},
},
};
これで、ContainerやCardの中に他のコンポーネントをドロップできるようになります。
注意点
良いことばかり書いてきましたが、Puckにも向き不向きがあります。
向いているケース
- Reactベースのプロジェクト
- 自社コンポーネントをそのまま使いたい
- データの所有権を自分で持ちたい
- カスタマイズ性が重要
- 商用利用でライセンス料を払いたくない
向いていないケース
- React以外のフレームワークを使っている
- ノンエンジニアだけでゼロから構築したい
- 既存のWordPressサイトに追加したい
個人的には、「Reactを使っているプロジェクトでページビルダーが必要になったら、まずPuckを検討すべき」だと思っています。
まとめ
正直なところ、Puckは「かゆいところに手が届く」ライブラリですね。
ノーコードツールの便利さと、開発者の柔軟性を両立している。既存のReactコンポーネントをそのまま使えるので、デザインシステムとの整合性も保てる。
以下の条件に当てはまるなら、試してみる価値は間違いなくあります:
- Reactプロジェクトでページビルダーが必要
- 既存のUIコンポーネントをそのまま使いたい
- SaaSのベンダーロックインを避けたい
- 商用利用でライセンス料を気にしたくない
- Next.jsと組み合わせて使いたい
10,200スター以上という数字が、その実用性を証明しています。705フォーク、69人のコントリビューターによる活発な開発も安心材料ですね。
まずはnpx create-puck-app my-appで試してみてください。驚くほど簡単にページビルダーが作れることがわかるはずです。
公式サイト: https://puckeditor.com GitHub: https://github.com/measuredco/puck デモ: https://demo.puckeditor.com/edit npm: https://www.npmjs.com/package/@measured/puck