Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Page Builder Bundle Laravel Package

openstudio/page-builder-bundle

A Laravel package that provides a flexible page builder for composing and managing content pages from reusable blocks, with bundled components and tools for defining, rendering, and editing page layouts in your application.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a Symfony bundle, not a Laravel package. There is no service provider, no vendor:publish, no facade. As a Laravel developer your job is to decide up front which half of the package you actually want:

  1. The GrapesJS integration layer (editor config, blocks, asset manager wiring) — genuinely reusable, it's just JS + config.
  2. The Symfony bridge (bundle class, DI extension, controllers, Doctrine persistence) — not reusable in Laravel; read it as a reference implementation.

Minimal steps

# Do NOT expect this to auto-register in Laravel. Inspect first:
composer show openstudio/page-builder-bundle --all
git clone https://github.com/openstudio-fr/page-builder-bundle /tmp/pb && cd /tmp/pb
cat composer.json          # look at require: symfony/framework-bundle, doctrine/*, twig/*
tree src/ -L 2             # separate "core" from "Bridge"/"DependencyInjection"

Then in your Laravel app, take the JS path only:

npm i grapesjs grapesjs-preset-webpage grapesjs-blocks-basic grapesjs-plugin-forms
{{-- resources/views/admin/pages/edit.blade.php --}}
<div id="gjs"></div>
@push('scripts')
<script type="module">
  import grapesjs from 'grapesjs';
  import 'grapesjs/dist/css/grapes.min.css';
  import presetWebpage from 'grapesjs-preset-webpage';

  const editor = grapesjs.init({
    container: '#gjs',
    plugins: [presetWebpage],
    storageManager: {
      type: 'remote',
      urlStore: '{{ route('admin.pages.store', $page) }}',
      urlLoad:  '{{ route('admin.pages.load',  $page) }}',
      storeAfterStop: true,
      contentType: 'application/json',
      headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
    },
  });
</script>
@endpush

Where to look first

  • composer.json — the require block tells you immediately whether the package can coexist with Laravel's Symfony components (it usually can't if it pins symfony/framework-bundle: ^7.0 and your app is on Symfony 6.x components via Laravel 10).
  • src/ top level — look for a folder that is not DependencyInjection, Resources/config, or Controller. Anything under a Model, Storage, Page, or Block namespace is the framework-agnostic core you might reuse.
  • Resources/ or assets/ — the GrapesJS config/blocks the bundle ships. This is the highest-value artifact: a curated block set you can copy into a config/grapesjs.php.
  • The README's config reference — mirror the keys into a Laravel config file so you're not hardcoding editor options in Blade.

First use case (a realistic 30-minute spike)

Build a single "landing page" CRUD:

Piece Where
pages table: id, slug, title, html, css, project_data JSON, published_at migration
GET /admin/pages/{page}/edit Blade view mounting GrapesJS
POST /admin/pages/{page} accepts { "grapes": {...projectData} }
GET /p/{slug} renders html + <style>{css}</style>

If that works, you've extracted 100% of the value this bundle offers a Laravel team. Everything else in the bundle is Symfony plumbing you'd rewrite anyway.


Implementation Patterns

Pattern 1 — Reference implementation, not a dependency

Treat the repo as documentation. Diff its GrapesJS config against the upstream GrapesJS defaults and copy the deltas:

// config/grapesjs.php  (mirroring the bundle's config tree)
return [
    'plugins'      => ['gjs-preset-webpage', 'gjs-blocks-basic', 'gjs-plugin-forms'],
    'pluginsOpts'  => [
        'gjs-preset-webpage' => ['modalImportTitle' => 'Import'],
    ],
    'canvas' => ['styles' => ['/css/site.css']],
    'deviceManager' => ['devices' => [
        ['name' => 'Desktop', 'width' => ''],
        ['name' => 'Tablet',  'width' => '768px', 'widthMedia' => '992px'],
        ['name' => 'Mobile',  'width' => '320px', 'widthMedia' => '480px'],
    ]],
];

Pattern 2 — Adapter over the package's storage interface

If the bundle ships a PageStorageInterface (check src/), implement it against Eloquent rather than fighting the Symfony service container:

final class EloquentPageStorage implements PageStorageInterface
{
    public function load(string $id): PageInterface
    {
        return Page::findOrFail($id)->toPageObject();
    }

    public function store(string $id, array $payload): void
    {
        Page::whereKey($id)->update([
            'project_data' => $payload['projectData'], // JSON, re-editable
            'html'         => $payload['html'],
            'css'          => $payload['css'],
        ]);
    }
}

The bundle's own Doctrine repository is then a worked example of what to store, which is more useful than the code itself.

Pattern 3 — Dual-column persistence (the pattern that actually matters)

Never store only HTML. GrapesJS's projectData is the source of truth for re-editing; html/css are a build artifact.

// Controller
$data = $request->validate([
    'grapes.projectData' => ['required', 'array'],
    'grapes.html'        => ['required', 'string'],
    'grapes.css'         => ['nullable', 'string'],
]);

$page->update([
    'project_data' => $data['grapes']['projectData'], // JSON column
    'html'         => $data['grapes']['html'],
    'css'          => $data['grapes']['css'],
    'rendered_at'  => now(),
]);

Client-side, force the payload shape with a custom storage manager so you control the wire format instead of accepting GrapesJS's default {"gjs-html": ...} envelope:

storageManager: {
  type: 'remote',
  options: {
    remote: {
      onStore: (data, editor) => ({
        grapes: {
          projectData: editor.getProjectData(),
          html: editor.getHtml(),
          css: editor.getCss(),
        },
      }),
      onLoad: (result) => result.data?.grapes?.projectData ?? {},
    },
  },
}

Pattern 4 — Blade partials as GrapesJS blocks

The most Laravel-native way to give editors a component library: render server-side partials and register them as blocks.

// A service that scans resources/views/blocks/*.blade.php
$blocks = collect(File::files(resource_path('views/blocks')))
    ->map(fn ($f) => [
        'id'       => $f->getFilenameWithoutExtension(),
        'label'    => str($f->getFilenameWithoutExtension())->headline(),
        'content'  => view("blocks.{$f->getFilenameWithoutExtension()}")->render(),
    ]);
{{-- resources/views/blocks/hero.blade.php --}}
<section class="hero">
  <h1>{{ $title ?? 'Headline' }}</h1>
  <a href="{{ $cta ?? '#' }}" class="btn">{{ $label ?? 'Get started' }}</a>
</section>

Ship blocks to the editor via @json($blocks) and editor.BlockManager.add(id, { content }).

Pattern 5 — Preview + publish pipeline

Mirror the "standalone / framework-agnostic" claim by making rendering a pure function:

final class PageRenderer
{
    public function render(Page $page): string
    {
        return view('pages.render', [
            'css'  => $page->css,
            'html' => $page->html,
        ])->render();
    }
}
  • Admin preview → PageRenderer with a robots: noindex layout.
  • Cache the rendered output (Cache::remember("page.{$page->id}", ...)) and bust on save.
  • Export to S3/static host if you want CDN-served pages.

Pattern 6 — Asset manager backed by your own uploads

Point GrapesJS at a Laravel upload route rather than the bundle's (likely Flysystem/Doctrine-backed) asset store:

assetManager: {
  upload: '/admin/media',
  uploadName: 'files',
  autoAddImage: true,
  customFetch: (url) => fetch(url).then(r => r.json()).then(d => d.data),
}

Return { data: [{ src, name, type }] } to match GrapesJS's expectation.

Integration tip: keep the Symfony side out of production

If you want to run the bundle to learn from it, do it in a throwaway Symfony 7 skeleton:

composer create-project symfony/skeleton:^7.0 /tmp/pb-demo
cd /tmp/pb-demo && composer require openstudio/page-builder-bundle
php bin/console debug:config openstudio_page_builder   # inspect the full config tree
php bin/console debug:router | grep -i page            # see what routes it registers

That debug:config output is the fastest way to learn every knob the package exposes — far faster than reading the extension class.


Gotchas and Tips

Framework mismatch is the primary risk

  • No Laravel auto-discovery. openstudio/page-builder-bundle follows the Symfony Bundle + Extension convention. There is no Illuminate\Support\ServiceProvider. php artisan vendor:publish will do nothing.
  • Composer conflicts are likely. The package requires Symfony 7 components. Laravel 10/11 pin symfony/* to ^6.4 or ^7.0 depending on version; a hard ^7.0 requirement on symfony/framework-bundle is not satisfiable in a Laravel app at all (Laravel does not depend on framework-bundle). composer require will fail or force you to --ignore-platform-reqs, which is worse.
  • Do not install it. Copy what you need. The only cost of not installing is losing composer update for the editor config, which changes rarely.

Licence

  • LGPL-3.0. Consuming it as a separate Composer dependency is fine (dynamic linking). Copy-pasting its source into your app/ directory makes your code a derivative work — if you lift code, prefer the JS config (which is likely trivial/uncopyrightable configuration) over PHP classes, and keep attribution.
  • If you modify and redistribute the package itself, LGPL requires you publish those modifications.

GrapesJS-specific pitfalls

  • CSS leakage. GrapesJS-generated CSS uses bare selectors (.hero { ... }). Rendering it into your app's master layout will clobber your admin UI. Always render pages in an isolated context: a separate layout, an iframe, or scope with editor.CssComposer post-processing / a #page-root prefix.
  • Absolute asset URLs. Images inserted via the Asset Manager are stored as absolute URLs. Switching from localhost to production, or adding a CDN, silently breaks every saved page. Normalise on save: strip the host and store root-relative paths, re-prefix at render time.
  • projectData grows unbounded. Users paste base64 images or huge SVG blocks. Use a LONGTEXT/JSON column, set max_allowed_packet awareness, and consider a size guard in FormRequest (e.g. max:2000000).
  • Silent save failures. GrapesJS's storage manager swallows non-2xx responses in some versions. Validate that your store endpoint responds 200 with a JSON body ({"data": ...}), and add a storageManager onError hook that surfaces a toast. This is the single most common "the editor is broken" report.
  • storeAfterStop vs storeOnChange. Defaults changed across GrapesJS versions. Explicitly set both; storeAfterStop: true is what you want for autosave.
  • Sanitisation. Even though only admins author pages, GrapesJS's "Custom code" block (from gjs-preset-webpage) allows arbitrary <script>. Sanitise at render with mews/purifier or run the HTML through HTMLPurifier. Do not trust html because it came from an admin.
  • Undo/redo + projectData mismatch. Calling editor.loadProjectData() after the editor has been initialised with components will not clear undo history — re-mount the editor on page switch, or call editor.UndoManager.clear().

Package-health quirks

  • 1 star, 0 dependents, last release dated 2026-09-27. The future-dated release is a red flag for release hygiene — verify tags actually exist and that composer require openstudio/page-builder-bundle:^1.0 resolves to something real. Treat the library as unmaintained until proven otherwise: pin an exact version, vendor the JS config you depend on, and assume you will own any bug you find.
  • No issue triage means no upstream fixes. Anything you need (a block, a config option, a bug in the storage adapter) you implement yourself. That's fine — the GrapesJS surface is well-documented upstream.
  • The "standalone, framework-agnostic" claim is worth verifying. Grep for Symfony\ inside the namespace you intend to reuse:
    grep -rl "use Symfony\\\\" src/ | sed 's|/[^/]*$||' | sort -u
    
    Any directory that comes back clean is reusable; anything else is Symfony-only.

Debugging checklist

Symptom Check
Editor renders, blocks empty BlockManager.getAll().length in console; plugin not registered
Save returns 419 CSRF token not sent in storageManager.headers
Save returns 422 Payload shape — inspect onStore output vs FormRequest rules
Page renders unstyled in app CSS leakage in reverse — your app's reset is overriding page CSS; scope it
Images 404 after deploy Absolute URL storage (see above)
loadProjectData throws JSON column returned as string; cast with 'project_data' => 'array' in the Eloquent model

Extension points worth knowing

  • GrapesJS plugins (grapesjs-preset-webpage, grapesjs-blocks-basic, grapesjs-tabs, grapesjs-custom-code, grapesjs-component-code-editor) do 90% of what the bundle's config does, with better docs and active maintenance.
  • editor.Commands.add for custom toolbar buttons (e.g. "Insert Blade partial", "Publish").
  • editor.CodeManager if you want a Tailwind-aware code editor pane.
  • Your own PageStorageInterface implementation is the cleanest seam if you ever want to swap the editor out — keep persistence behind an interface even if you never swap.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager