An open API service indexing awesome lists of open source software.

https://github.com/coders-tm/laravel-page-builder

A powerful Laravel page builder using sections, layouts and JSON rendering. Build dynamic pages with a visual editor, reusable sections and multi-layout support.
https://github.com/coders-tm/laravel-page-builder

blade blade-template cms content-management drag-and-drop landing-page-builder laravel laravel-cms laravel-package multi-theme page-builder schema-driven theme-system visual-editor website-builder website-editor

Last synced: about 1 month ago
JSON representation

A powerful Laravel page builder using sections, layouts and JSON rendering. Build dynamic pages with a visual editor, reusable sections and multi-layout support.

Awesome Lists containing this project

README

          

# Laravel Page Builder


Build Status
Total Downloads
Latest Stable Version
License


Intro

A modern page builder for Laravel that allows you to build dynamic pages using layouts, sections and JSON rendering.
It includes a visual editor, layout system, reusable sections and multi-theme support.

## Features

- **Blade-native rendering** — sections and blocks are regular Blade views with typed PHP objects
- **`@schema()` directive** — declare settings, child blocks, and presets directly in Blade templates
- **Visual editor** — React SPA with iframe live preview, drag-and-drop, and inline text editing
- **JSON-based storage** — page data stored as JSON files on disk for fast reads and easy version control
- **JSON templates** — fallback layouts for pages without a per-page JSON; supports `wrapper`, variable interpolation (`{{ $page->title }}`), and theme overrides
- **Per-page Layouts** — site header and footer are configurable per-page, stored in the page JSON
- **Recursive block nesting** — container blocks (rows, columns) can hold child blocks to any depth
- **Theme blocks** — register global block types that any section can accept via `@theme` wildcard
- **21+ Field Types** — from basic text inputs to advanced color pickers, icon selectors, and custom types
- **Page Meta Persistence** — SEO titles and descriptions are automatically managed and persisted across dynamic and preserved pages
- **Editor mode** — `data-editor-*` attributes injected only when the editor is active
- **Publishable assets** — config, views, migrations, and frontend assets can be published independently

## Requirements

- PHP 8.2+
- Laravel 11.x or 12.x

## Installation

```bash
composer require coderstm/laravel-page-builder
```

The package auto-registers its service provider via Laravel's package discovery.

### Run the install command

```bash
php artisan pagebuilder:install
```

This single command:

1. Publishes `config/pagebuilder.php`
2. Publishes database migrations
3. Publishes the compiled editor frontend assets to `public/pagebuilder/`
4. Scaffolds default starter views into your app:
- `resources/views/layouts/page.blade.php` — base HTML layout
- `resources/views/sections/` — announcement, header, hero, rich-text, content, footer
- `resources/views/blocks/` — row, column, text

**Options**

| Flag | Description |
| ----------- | ------------------------------------------------------ |
| `--force` | Overwrite files that already exist |
| `--migrate` | Run `php artisan migrate` immediately after publishing |

```bash
# Overwrite existing files and run migrations in one step
php artisan pagebuilder:install --force --migrate
```

### Run migrations (if not using `--migrate`)

```bash
php artisan migrate
```

### Configuration reference

`config/pagebuilder.php` is published to your application's `config/` directory:

```php
return [
// Path to page JSON data files
'pages' => resource_path('views/pages'),

// Path to section Blade templates
'sections' => resource_path('views/sections'),

// Path to theme block Blade templates
'blocks' => resource_path('views/blocks'),

// Path to JSON template files (fallback layouts for pages without a page JSON)
'templates' => resource_path('views/templates'),

// Middleware applied to editor routes
'middleware' => ['web'],

// Filesystem disk for asset uploads
'disk' => 'public',

// Directory within the disk for uploaded assets
'asset_directory' => 'pagebuilder',

// Reserved slugs that cannot be used for dynamic pages
'preserved_pages' => ['home'],
];
```

### Publish resources individually

If you need to re-publish a specific resource:

```bash
# Config
php artisan vendor:publish --tag=pagebuilder-config

# Database migrations
php artisan vendor:publish --tag=pagebuilder-migrations

# Editor frontend assets (React SPA)
php artisan vendor:publish --tag=pagebuilder-assets

# Built-in package views
php artisan vendor:publish --tag=pagebuilder-views
```

---

## Creating Sections

Sections are the top-level building blocks of a page. Each section is a Blade view that declares its schema using the `@schema()` directive.

### 1. Create the Blade file

Place section templates in the configured sections directory (default: `resources/views/sections/`).

```blade
{{-- resources/views/sections/hero.blade.php --}}
@schema([
'name' => 'Hero',
'settings' => [
['id' => 'title', 'type' => 'text', 'label' => 'Title', 'default' => 'Welcome'],
['id' => 'subtitle', 'type' => 'text', 'label' => 'Subtitle', 'default' => ''],
['id' => 'bg_color', 'type' => 'color', 'label' => 'Background Color', 'default' => '#ffffff'],
],
'blocks' => [
['type' => 'row'],
['type' => '@theme'],
],
'presets' => [
['name' => 'Hero'],
['name' => 'Hero with Row', 'blocks' => [
['type' => 'row', 'settings' => ['columns' => '2']],
]],
],
])

editorAttributes() !!}
style="background-color: {{ $section->settings->bg_color }}">


{{ $section->settings->title }}


{{ $section->settings->subtitle }}


@blocks($section)

```

### 2. Understanding the `@schema()` array

| Key | Type | Description |
| ------------ | ------ | ------------------------------------------------------------ |
| `name` | string | **Required.** Human-readable name shown in the editor |
| `settings` | array | Setting definitions with `id`, `type`, `label`, `default` |
| `blocks` | array | Allowed child block types (inline definitions or theme refs) |
| `presets` | array | Pre-configured templates shown in the "Add section" picker |
| `max_blocks` | int | Maximum number of child blocks allowed |

### 3. Section template API

| Property / Method | Description |
| ------------------------------ | ---------------------------------------------------------- |
| `$section->id` | Unique instance ID |
| `$section->type` | Section type identifier (matches filename) |
| `$section->name` | Human-readable name from schema |
| `$section->settings->key` | Typed setting access with automatic defaults |
| `$section->blocks` | `BlockCollection` of hydrated top-level blocks |
| `$section->editorAttributes()` | Editor `data-*` attributes (empty string when not editing) |
| `@blocks($section)` | Renders all top-level blocks |

---

## Creating Blocks

Blocks are reusable components that live inside sections (or inside other blocks). Block Blade files live in the configured blocks directory (default: `resources/views/blocks/`).

### Theme Blocks

Theme blocks are registered globally and can be referenced by any section that declares `['type' => '@theme']` in its `blocks` array.

```blade
{{-- resources/views/blocks/row.blade.php --}}
@schema([
'name' => 'Row',
'settings' => [
[
'id' => 'columns',
'type' => 'select',
'label' => 'Columns',
'default' => '2',
'options' => [
[
'value' => '1',
'label' => '1 Column',
],
['value' => '2', 'label' => '2 Columns'],
['value' => '3', 'label' => '3 Columns'],
],
],
[
'id' => 'gap',
'type' => 'select',
'label' => 'Gap',
'default' => 'md',
'options' => [
[
'value' => 'none',
'label' => 'None',
],
['value' => 'sm', 'label' => 'Small'],
['value' => 'md', 'label' => 'Medium'],
['value' => 'lg', 'label' => 'Large'],
],
],
],
'blocks' => [
[
'type' => 'column',
'name' => 'Column',
],
],
'presets' => [
[
'name' => 'Two Columns',
'settings' => ['columns' => '2'],
'blocks' => [
[
'type' => 'column',
],
['type' => 'column'],
],
],
[
'name' => 'Three Columns',
'settings' => ['columns' => '3'],
'blocks' => [
[
'type' => 'column',
],
['type' => 'column'],
['type' => 'column'],
],
],
],
])

editorAttributes() !!}
class="grid grid-cols-{{ $block->settings->columns }} gap-{{ $block->settings->gap }}">
@blocks($block)

```

```blade
{{-- resources/views/blocks/column.blade.php --}}
@schema([
'name' => 'Column',
'settings' => [
['id' => 'padding', 'type' => 'select', 'label' => 'Padding', 'default' => 'none',
'options' => [
['value' => 'none', 'label' => 'None'],
['value' => 'sm', 'label' => 'Small'],
['value' => 'md', 'label' => 'Medium'],
['value' => 'lg', 'label' => 'Large'],
]],
],
'blocks' => [
['type' => '@theme'],
],
])

editorAttributes() !!} class="p-{{ $block->settings->padding }}">
@blocks($block)

```

### Block template API

| Property / Method | Description |
| ---------------------------- | ------------------------------------------------ |
| `$block->id` | Unique block instance ID |
| `$block->type` | Block type identifier (matches filename) |
| `$block->settings->key` | Typed setting access with defaults |
| `$block->blocks` | `BlockCollection` of nested child blocks |
| `$block->editorAttributes()` | Editor `data-*` attributes |
| `$section` | Parent section (always available in block views) |
| `@blocks($block)` | Renders child blocks of this container |

### Block Detection: Local vs Theme Reference

In `@schema` `blocks` arrays, entries are detected as either **local definitions** or **theme-block references**:

| Entry | Type | Detection |
| --------------------------------------------------------------- | ---------------- | -------------------------------------------------- |
| `['type' => 'column']` | Theme reference | Only has `type` key → resolved from block registry |
| `['type' => '@theme']` | Wildcard | Accepts any registered theme block |
| `['type' => 'column', 'name' => 'Column', 'settings' => [...]]` | Local definition | Has extra keys → used as-is |

---

## Page JSON Structure

Pages are stored as JSON files in the configured pages directory. Each page contains sections, their settings, nested blocks, and render order.

```json
{
"title": "Home",
"meta": {
"description": "Welcome to our site"
},
"sections": {
"hero-1": {
"type": "hero",
"settings": {
"title": "Welcome",
"subtitle": "Build amazing pages",
"bg_color": "#f0f0f0"
},
"blocks": {
"row-1": {
"type": "row",
"settings": { "columns": "2", "gap": "md" },
"blocks": {
"col-left": {
"type": "column",
"settings": { "padding": "md" },
"blocks": {}
},
"col-right": {
"type": "column",
"settings": { "padding": "md" },
"blocks": {}
}
},
"order": ["col-left", "col-right"]
}
},
"order": ["row-1"]
}
},
"order": ["hero-1"]
}
```

---

## Templates

Templates are **JSON fallback layouts** for pages that have no per-page page builder JSON file and no custom Blade view. They let you define a single file that controls which sections a whole category of pages renders — without requiring a separate `pages/{slug}.json` for every page.

### Page resolution order

```
1. Custom Blade view pages/{slug}.blade.php (highest priority)
2. Page builder JSON pages/{slug}.json
3. Template JSON templates/{template}.json or templates/page.json
4. 404
```

Templates are only consulted when both step 1 and step 2 miss. A template never overrides an existing page JSON.

### Creating a template

Place template files in `resources/views/templates/` (configurable via `config('pagebuilder.templates')`).

```json
// resources/views/templates/page.json — default template used by all pages
{
"sections": {
"main": {
"type": "page-content"
}
},
"order": ["main"]
}
```

The `page.json` file is the **default template**. Any page without a page JSON, and without a specific template selected, renders through it.

### Template JSON schema

| Field | Type | Required | Description |
|---|---|---|---|
| `sections` | object | yes | Section data map — same format as page JSON sections |
| `order` | string[] | yes | Section render order |
| `layout` | string \| false | no | Layout type (e.g. `"page"`, `"full-width"`). Defaults to `"page"`. Pass `false` to render without header/footer zones |
| `wrapper` | string | no | CSS-selector string that wraps all sections in an HTML element |

### Assigning a template to a page

Set the `template` column on the `Page` model:

```php
$page = Page::find(1);
$page->template = 'page.alternate';
$page->save();
```

Or when creating a page:

```php
Page::create([
'title' => 'About Us',
'slug' => 'about',
'template' => 'page.alternate',
'content' => '

About our company.

',
]);
```

Template names map to filenames without the `.json` extension:

| `template` field | File loaded |
|---|---|
| `null` or `""` | `templates/page.json` |
| `"page"` | `templates/page.json` |
| `"page.alternate"` | `templates/page.alternate.json` |
| `"product"` | `templates/product.json` |

If the selected template file does not exist, the package falls back to `page.json`. If `page.json` also does not exist, a 404 is returned.

### The `wrapper` property

The `wrapper` field wraps all rendered section HTML in a single HTML element. The value uses a CSS-selector-like syntax:

```
tag#id.class1.class2[attr1=val1][attr2=val2]
```

Supported wrapper tags: `

`, ``, ``.

```json
{
"wrapper": "div#div_id.div_class[attribute-one=value]",
"sections": { "main": { "type": "page-content" } },
"order": ["main"]
}
```

Output:

```html




```

### Variable interpolation

Template section settings support `{{ $page->attribute }}` placeholders. At render time they are replaced with the corresponding attribute from the `Page` Eloquent model.

```json
{
"sections": {
"hero": {
"type": "hero",
"settings": {
"title": "{{ $page->title }}",
"description": "{{ $page->meta_description }}"
}
},
"main": { "type": "page-content" }
},
"order": ["hero", "main"]
}
```

Any column on the `Page` model can be used: `title`, `slug`, `content`, `meta_title`, `meta_description`, `meta_keywords`, or any custom column. Missing or `null` attributes resolve to an empty string.

### Alternative template example

```json
// resources/views/templates/page.alternate.json
{
"wrapper": "main#page-alternate.page-wrapper",
"sections": {
"main": {
"type": "page-content"
}
},
"order": ["main"]
}
```

### `layout: false` — rendering without header/footer

Set `"layout": false` to skip the layout zone system entirely. No `@sections('header')` or `@sections('footer')` zones are rendered:

```json
{
"layout": false,
"sections": {
"main": { "type": "hero" }
},
"order": ["main"]
}
```

### Theme-aware templates

If a theme is active, `TemplateStorage` checks the theme's `views/templates/` directory first. This allows themes to override the default `page.json` template or add new template files without touching the application's templates directory:

```
themes/my-theme/views/templates/page.json ← overrides app templates/page.json
themes/my-theme/views/templates/product.json ← theme-specific product template
```

---

## Rendering Pages

### In Controllers

```php
use Coderstm\PageBuilder\Facades\Page;

class PageController extends Controller
{
public function show(string $slug)
{
return Page::render($slug);
}
}
```

### Programmatic Page Rendering

```php
use Coderstm\PageBuilder\Facades\Page;

// Render from slug (loads JSON from disk)
$html = Page::render('home');

// Render with extra meta passed to the page model/template
$html = Page::render('home', ['title' => 'My Home Page']);
```

---

## Registering Additional Paths

You can register additional directories for section and block discovery:

```php
use Coderstm\PageBuilder\Facades\Section;
use Coderstm\PageBuilder\Facades\Block;

// In a service provider's boot() method
Section::add(resource_path('views/custom-sections'));
Block::add(resource_path('views/custom-blocks'));
```

### Manual Registration

Register a section or block programmatically without a Blade file:

```php
use Coderstm\PageBuilder\Facades\Section;
use Coderstm\PageBuilder\Schema\SectionSchema;

Section::register('custom-hero', new SectionSchema([
'name' => 'Custom Hero',
'settings' => [
['id' => 'title', 'type' => 'text', 'label' => 'Title', 'default' => 'Hello'],
],
]), 'my-views::sections.custom-hero');
```

---

## Setting Types

The `@schema` settings array supports these built-in types:

| Type | Description | Extra Keys |
| ------------------ | -------------------------------- | --------------------------- |
| `text` | Single-line text input | — |
| `textarea` | Multi-line text input | — |
| `richtext` | Rich text editor (multi-line) | — |
| `inline_richtext` | Rich text editor (single-line) | — |
| `select` | Dropdown select | `options: [{value, label}]` |
| `radio` | Radio buttons | `options: [{value, label}]` |
| `checkbox` | Boolean toggle | — |
| `range` | Numeric slider | `min`, `max`, `step` |
| `number` | Number input | `min`, `max`, `step` |
| `color` | Color picker (hex) | — |
| `color_background` | CSS background (gradients) | — |
| `image_picker` | Media library selector | — |
| `url` | Link/URL input | — |
| `video_url` | YouTube/Vimeo URL | — |
| `icon_fa` | FontAwesome icon picker | — |
| `icon_md` | Material Design icon picker | — |
| `text_alignment` | Left/Center/Right segmented ctrl | — |
| `html` | Raw HTML code editor | — |
| `blade` | Blade template code editor | — |
| `header` | Sidebar section divider | `content` |
| `paragraph` | Sidebar informational text | `content` |
| `external` | Dynamic API-driven selector | — |

---

## Editor

### Accessing the Editor

The editor is available at:

```
GET /pagebuilder/{slug?}
```

Protect it with authentication middleware in your config:

```php
// config/pagebuilder.php
'middleware' => ['web', 'auth'],
```

### Editor API Endpoints

| Method | URL | Description |
| ------ | ----------------------------- | --------------------- |
| `GET` | `/pagebuilder/pages` | List all pages |
| `GET` | `/pagebuilder/page/{slug}` | Get page JSON |
| `POST` | `/pagebuilder/render-section` | Live-render a section |
| `POST` | `/pagebuilder/save-page` | Save page JSON |
| `GET` | `/pagebuilder/assets` | List uploaded assets |
| `POST` | `/pagebuilder/assets/upload` | Upload an asset |

### Editor Helpers

```php
// Check if editor mode is active
pb_editor(); // Returns bool

// In Blade templates
@if(pb_editor())
{{-- Editor-only content --}}
@endif
```

---

## Custom Asset Providers

By default the editor stores uploaded assets through the built-in Laravel provider (`storage/app/public/pagebuilder`). You can replace it with any storage backend — S3, Cloudflare R2, Cloudinary, DigitalOcean Spaces — by passing a custom provider to `PageBuilder.init()`.

### Provider interface

A provider is a plain JavaScript object with two async methods:

```js
const myProvider = {
// Return a paginated list of assets
async list({ page = 1, search = "" } = {}) {
// Must return: { data: Asset[], pagination: { page, per_page, total } }
},

// Upload a File object, return the stored asset
async upload(file) {
// Must return: { id, name, url, thumbnail, size, type }
},
};
```

The `url` field is what gets stored in page JSON and rendered in Blade — it must be a publicly accessible URL.

### Registering the provider

```html

PageBuilder.init({
baseUrl: "/pagebuilder",
assets: {
provider: myProvider,
},
});

```

### AWS S3 / DigitalOcean Spaces / Cloudflare R2

Keep uploads server-side through a thin Laravel proxy controller that writes to S3 using `Storage::disk('s3')`:

```js
const s3Provider = {
async list({ page = 1, search = "" } = {}) {
const q = new URLSearchParams({ page, q: search });
const res = await fetch(`/api/pagebuilder/assets?${q}`);
if (!res.ok) throw new Error("Failed to fetch assets");
return res.json();
},
async upload(file) {
const body = new FormData();
body.append("file", file);
const res = await fetch("/api/pagebuilder/assets/upload", {
method: "POST",
headers: {
"X-CSRF-TOKEN":
document
.querySelector('meta[name="csrf-token"]')
?.getAttribute("content") ?? "",
},
body,
});
if (!res.ok) throw new Error("Upload failed");
return res.json();
},
};
```

For Spaces/R2, configure the S3-compatible endpoint in `.env` — no JS changes required:

```env
AWS_ENDPOINT=https://nyc3.digitaloceanspaces.com # Spaces
# or
AWS_ENDPOINT=https://.r2.cloudflarestorage.com # R2
AWS_USE_PATH_STYLE_ENDPOINT=true
```

### Cloudinary (direct browser upload)

```js
const cloudinaryProvider = {
async list({ page = 1, search = "" } = {}) {
const q = new URLSearchParams({ page, q: search });
const res = await fetch(`/api/pagebuilder/cloudinary/assets?${q}`);
if (!res.ok) throw new Error("Failed to fetch assets");
return res.json();
},
async upload(file) {
// Get a signed upload preset from your Laravel backend
const sigRes = await fetch("/api/pagebuilder/cloudinary/sign", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN":
document
.querySelector('meta[name="csrf-token"]')
?.getAttribute("content") ?? "",
},
body: JSON.stringify({ filename: file.name }),
});
const { signature, timestamp, cloudName, apiKey, folder } =
await sigRes.json();

const body = new FormData();
body.append("file", file);
body.append("api_key", apiKey);
body.append("timestamp", timestamp);
body.append("signature", signature);
body.append("folder", folder);

const up = await fetch(
`https://api.cloudinary.com/v1_1/${cloudName}/image/upload`,
{ method: "POST", body },
);
if (!up.ok) throw new Error("Cloudinary upload failed");
const d = await up.json();

return {
id: d.public_id,
name: d.original_filename,
url: d.secure_url,
thumbnail: d.secure_url.replace(
"/upload/",
"/upload/w_200,h_200,c_fill/",
),
size: d.bytes,
type: `${d.resource_type}/${d.format}`,
};
},
};
```

For the full provider contract and additional examples, see the [Developer Documentation](docs/index.md).

---

## Blade Directives

| Directive | Description |
| ------------------- | ----------------------------------------------------------------- |
| `@blocks($section)` | Renders all top-level blocks of a section |
| `@blocks($block)` | Renders child blocks inside a container block |
| `@schema([...])` | Declares schema (no-op at render time, extracted at registration) |
| `@pbEditorClass` | Outputs CSS class when editor mode is active |

---

## Architecture Reference

### Key Classes

| Class | Responsibility |
| ----------------- | ----------------------------------------------------------------- |
| `SectionRegistry` | Discovers section Blade files, extracts schemas, provides lookup |
| `BlockRegistry` | Discovers block Blade files, extracts schemas, provides lookup |
| `Renderer` | Core rendering engine: hydrates JSON → objects, renders via Blade |
| `PageRenderer` | Loads page JSON, renders all enabled sections in order |
| `PageStorage` | Reads/writes page JSON files to disk |
| `PagePublisher` | Compiles pages into static Blade files |
| `PageBuilder` | Static API for editor mode, CSS/JS asset URLs |

---

## Reporting Issues

When reporting bugs, please include:

- PHP and Laravel versions
- Package version
- Steps to reproduce
- Expected vs actual behavior
- Relevant error messages or logs

---

## Layout Sections

Pages can define a `layout` key for per-page overrides of structural slots (header, footer) that live **outside** the main `@yield('content')` block in your Blade layout.

```json
{
"sections": { "..." },
"order": ["hero"],
"layout": {
"type": "page",
"header": {
"sections": {
"header": {
"type": "site-header",
"settings": { "sticky": true },
"blocks": {},
"order": [],
"disabled": false
}
}
},
"footer": {
"sections": {
"footer": {
"type": "site-footer",
"settings": {},
"blocks": {},
"order": [],
"disabled": false
}
}
},
}
}
```

Render layout sections in your Blade layout file using `@sections()`:

```blade
{{-- resources/views/layouts/page.blade.php --}}








{{ $meta_title ?? ($title ?? '') . ' | ' . config('app.name') }}






...

@stack('content_for_head')

@sections('header')

@yield('content')

@sections('footer')

```

Layout sections are **non-sortable** — their position is determined by the Blade layout. In the editor they appear as fixed rows above and below the sortable page section list.

**Rules:**

- Keys that match `"header"` or carry `position: "top"` render in the top zone; everything else goes to the bottom zone.
- `disabled: true` causes `@sections()` to return an empty string for that slot.
- `_name` overrides the schema display name in the editor (same as page sections).

---

## Theme Integration

The package integrates with [qirolab/laravel-themer](https://github.com/qirolab/laravel-themer) for multi-theme support.

### Register Theme Sections and Blocks

If you're using a theme system you can set the active theme and the package will automatically register theme `sections` and `blocks` when the expected view paths exist. This is convenient when using a theme package or `qirolab/laravel-themer`.

```php
use Coderstm\PageBuilder\Facades\Theme;
use Coderstm\PageBuilder\Facades\Section;
use Coderstm\PageBuilder\Facades\Block;

// Set the active theme (for example in a ThemeServiceProvider or middleware)
Theme::set('my-theme');

// The package will automatically register the following directories if they exist:
// themes/my-theme/views/sections
// themes/my-theme/views/blocks

// If you need to register additional paths manually you can still call:
Section::add(base_path('themes/my-theme/views/sections'));
Block::add(base_path('themes/my-theme/views/blocks'));
```

### Global Theme Settings

Define global design tokens (colors, fonts, spacing) in `config/pagebuilder.php`. Settings are grouped for display in the editor's Theme Settings panel:

```php
'theme_settings_schema' => [
[
'name' => 'Colors',
'settings' => [
[
'key' => 'colors.primary',
'label' => 'Primary',
'type' => 'color',
'default' => '#10b981',
'css_var' => '--colors-primary',
],
[
'key' => 'colors.background_dark',
'label' => 'Background (dark)',
'type' => 'color',
'default' => '#0f0f0f',
'css_var' => '--colors-background-dark',
],
],
],
[
'name' => 'Typography',
'settings' => [
[
'key' => 'fonts.body',
'label' => 'Body font',
'type' => 'google_font',
'default' => 'Inter, sans-serif',
'css_var' => '--fonts-body',
],
],
],
[
'name' => 'Radius & Shape',
'settings' => [
[
'key' => 'radius.base',
'label' => 'Radius (base)',
'type' => 'text',
'default' => '0.25rem',
'css_var' => '--radius-base',
],
],
],
],
```

**Schema fields**

| Field | Required | Description |
| --------- | -------- | --------------------------------------------------------------------------- |
| `key` | Yes | Dot-notation key used to store and retrieve the value (`colors.primary`) |
| `type` | Yes | Field type: `color`, `text`, `select`, `google_font`, etc. |
| `label` | Yes | Human-readable label shown in the editor panel |
| `default` | Yes | Fallback value used when no override has been saved |
| `css_var` | No | CSS custom property (e.g. `--colors-primary`) updated live in the preview |

**`css_var` — live preview sync**

When a `css_var` is declared on a setting, the editor updates that CSS custom property on the preview iframe's `:root` in real time as the user types — no page reload required. Declare your tokens in your theme stylesheet to consume them:

```css
:root {
--colors-primary: #10b981;
--fonts-body: Inter, sans-serif;
--radius-base: 0.25rem;
}

.btn-primary { background-color: var(--colors-primary); }
body { font-family: var(--fonts-body); }
.card { border-radius: var(--radius-base); }
```

**`google_font` setting type**

Use `type: 'google_font'` to let editors pick a Google Font from a curated library. The selected font is automatically injected as a `` tag in the page `` via the `@pbThemeFont` Blade directive:

```blade
{{-- in your layout --}}
@pbThemeFont
```

**Accessing values in Blade**

`$theme` is a `ThemeSettings` instance shared with all Blade views:

```blade

:root {
--colors-primary: {{ $theme->get('colors.primary', '#10b981') }};
--fonts-body: {{ $theme->get('fonts.body', 'Inter, sans-serif') }};
}

```

Use `$theme->get('key', 'default')` for dot-notation access with a fallback, or `$theme->key` for top-level keys.

**Editor reset options**

In the Theme Settings panel editors can:
- **Reset individual setting** — hover a setting row and click the reset icon to restore its `default` value.
- **Reset all** — click **Reset all** in the panel header to restore every setting to its schema default in one action. Both reset paths trigger live CSS var updates immediately.

### Theme Middleware

You can use the provided `ThemeMiddleware` to automatically apply themes based on route parameters or session data.

```php
// routes/web.php
Route::get('/shop/{theme_slug}', function () {
// ...
})->middleware('theme:theme_slug');
```

---

## Artisan Commands

| Command | Description |
| ------------------------------- | --------------------------------------------------------------------------- |
| `pagebuilder:install` | Publish config, migrations, assets, and scaffold starter views |
| `pagebuilder:install --force` | Same as above, overwriting any existing files |
| `pagebuilder:install --migrate` | Also run `php artisan migrate` after publishing |
| `pages:regenerate` | Rebuild the page registry cache (run after adding/removing page JSON files) |
| `theme:link` | Symlink theme asset directories into `public/themes/` |
| `theme:link --force` | Overwrite existing symlinks |

---

## License

This package is released under a **Non-Commercial Open Source License**.

- Free to use, modify, and distribute for **non-commercial purposes**.
- **Commercial use is not permitted** without a separate license agreement.
- Contact [hello@dipaksarkar.in](mailto:hello@dipaksarkar.in) for commercial licensing.

See [LICENSE.md](LICENSE.md) for the full license text.