cybertroniankelvin/graper
Filament plugin that brings a GrapesJS v3 drag‑and‑drop page builder to your admin panel. Create pages with blocks (hero, CTA, testimonials, etc.), edit on-canvas, save to the database, publish by slug, and register your own custom blocks.
composer require cybertroniankelvin/graper
php artisan graper:install
app/Providers/Filament/AdminPanelProvider.php:
public function panel(Panel $panel): Panel {
return $panel->plugins([
GraperPlugin::make(),
]);
}
summer-sale-2024/pages/summer-sale-2024 to preview.html, css, and project_data in the graper_pages table.is_published.// Programmatically create a page
$page = GraperPage::create([
'title' => 'Product Launch',
'slug' => 'product-launch-2024',
'is_published' => true,
'html' => '<section class="hero">...</section>',
'css' => '.hero { background: #ff0000; }',
'project_data' => json_encode(['blocks' => [...]]),
]);
App\Blocks\ProductCarouselBlock).getId(), getTemplate(), etc.).BlockRegistry::make()->register(ProductCarouselBlock::class);
class ProductCarouselBlock extends Block {
public static function getId(): string => 'product-carousel';
public static function getName(): string => 'Product Carousel';
public static function getTemplate(): string {
return <<<'HTML'
<div class="carousel">
@foreach($products as $product)
<div class="carousel-item">
<img src="{{ $product->image }}" alt="{{ $product->name }}">
<h3>{{ $product->name }}</h3>
</div>
@endforeach
</div>
HTML;
}
}
@php
$page = \CybertronianKelvin\Graper\Models\GraperPage::where('slug', 'about')->first();
@endphp
<div class="page-content">
{!! $page->html !!}
<style>{!! $page->css !!}</style>
</div>
Route::get('/landing/{slug}', [GraperPageController::class, 'display']);
project_data to pass dynamic variables to blocks.project_data (JSON field):
$page->project_data = json_encode([
'products' => Product::all()->toArray(),
'promo_code' => 'SUMMER20'
]);
public function getTemplate(): string {
return <<<'HTML'
<div class="promo-banner">
Use code: <strong>{{ $promo_code }}</strong>
</div>
HTML;
}
project_data.public function handle(Request $request, Closure $next) {
if ($request->user()->is_testing_ab) {
return redirect()->route('pages.show', 'variant-b');
}
return $next($request);
}
// Route: /pages/{slug}?variant={variant}
$variant = $request->query('variant');
$page = GraperPage::where('slug', $variant ?? $slug)->first();
GraperPageResource by publishing and extending:
php artisan vendor:publish --tag=graper-config --force
app/Filament/Resources/GraperPageResource.php to add custom fields or actions.public function getTemplate(): string {
return '<div class="bg-blue-500 text-white p-8">...</div>';
}
css field in the page model.public function getTemplate(): string {
return '<img src="' . $this->getImageUrl() . '" alt="Hero">';
}
private function getImageUrl(): string {
return Storage::disk('public')->url('filament-media/hero.jpg');
}
project_data:
{
"translations": {
"en": { "title": "Summer Sale", "cta": "Shop Now" },
"es": { "title": "Oferta de Verano", "cta": "Comprar Ahora" }
}
}
app()->getLocale().$page = Cache::remember("page:{$slug}", now()->addHours(1), function() use ($slug) {
return GraperPage::where('slug', $slug)->first();
});
getTemplate() may expose XSS risks.public function getTemplate(): string {
return <<<'HTML'
<div>{!! \Illuminate\Support\HtmlString::from($this->getSanitizedContent()) !!}</div>
HTML;
}
css field may override Tailwind or block-specific styles..block-hero { ... }).!important sparingly; prefer specificity.project_data is stored as JSON, requiring manual parsing in templates.public function getData(): array {
return json_decode($this->page->project_data ?? '{}', true);
}
/pages/{slug} route may conflict with existing routes.config/graper.php:
'page_route_prefix' => 'marketing', // Now accessible at /marketing/{slug}
How can I help you explore Laravel packages today?