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
pollora-blocks
pollora/nectar
claude-code
gutenberg
wordpress-blocks
pollora
vite
blade
dynamic-blocks
block-json

Create Gutenberg blocks in Pollora themes, plugins and modules with Vite, JSX/TSX, Blade rendering and Tailwind CSS.

Install
php artisan boost:add-skill pollora/nectar

Save this content to: .claude/skills/pollora-blocks/SKILL.md

# Pollora Block Development

## When to use this skill
Use this skill when creating, configuring, or customizing WordPress Gutenberg blocks within a Pollora theme, plugin or module.

## Generating a Block

```bash
php artisan pollora:make:block hero-banner --theme
```

Options:
- `--theme[=NAME]` — Create in a theme (default: the active theme)
- `--plugin=NAME` — Create in a plugin
- `--static` — A static block saved in `post_content` (`save.jsx`, no `render.blade.php`)
- `--inner-blocks` — Add inner blocks: `<InnerBlocks />` in `render.blade.php` (Gutenberg's `InnerBlocks` in `edit.jsx`/`save.jsx` with `--static`)
- `--namespace=NS`, `--title=TITLE`, `--category=CAT`, `--icon=ICON`, `--no-view-script`, `--force`
- `--dynamic` is deprecated: blocks are dynamic by default

On the first block, the command patches `vite.config.js` (block entries, `wordpressPlugin()`, Blade-only full reloads) and adds the npm dependencies. It refuses a theme or plugin with no `package.json` or no `vite.config.js`.

## Block Structure

```
resources/views/blocks/hero-banner/
├── block.json           # WordPress block metadata
├── render.blade.php     # Server-side render (default)
├── index.jsx            # Entry point & registration
├── edit.jsx             # Editor component — shows what the page will show
├── save.jsx             # Static blocks only (--static)
├── editor.css           # Editor-only styles
├── style.css            # Shared frontend + editor styles
└── view.js              # Frontend-only script (optional)
```

Blocks are **dynamic by default**: `render.blade.php` renders them on each request, so their markup is not stored in `post_content`. Changing the markup updates every existing block instead of triggering "This block contains unexpected or invalid content".

## block.json

```json
{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "my-theme/hero-banner",
    "version": "1.0.0",
    "title": "Hero Banner",
    "category": "theme",
    "icon": "cover-image",
    "supports": {
        "html": false,
        "align": ["wide", "full"]
    },
    "attributes": {
        "heading": { "type": "string", "default": "" },
        "ctaText": { "type": "string", "default": "Learn More" },
        "ctaUrl": { "type": "string", "default": "#" }
    },
    "textdomain": "my-theme",
    "editorScript": "file:./index.jsx",
    "editorStyle": "file:./editor.css",
    "style": "file:./style.css",
    "viewScript": "file:./view.js",
    "render": "file:./render.blade.php"
}
```

## Editor Component (edit.jsx) and save

A generated dynamic block is edited through the framework's editor runtime, `window.pollora.blocks`, which Pollora loads with every block that has a `render` template (handle `pollora-block-editor`). Nothing to install or import:

```jsx
// edit.jsx
import metadata from './block.json';

export default window.pollora.blocks.bladeEdit(metadata);

// index.jsx
registerBlockType(metadata.name, {
    edit: Edit,
    save: window.pollora.blocks.save, // stores the inner blocks, nothing else
});
```

`bladeEdit` previews `render.blade.php` through the core block-renderer REST route (again 200 ms after an attribute change) and makes its `<InnerBlocks />` editable in place. Framework v13.32.0-beta.8 and earlier generate `ServerSideRender` and `save: () => null` instead — check `edit.jsx` before assuming.

## Rendering with Blade (render.blade.php)

The template receives `$attributes` (array), `$content` (inner blocks HTML), `$block` (`WP_Block`) and `$isPreview` (true while it renders for the editor's preview). Blade components work as in any view.

```blade
<section {!! get_block_wrapper_attributes(['class' => 'py-16']) !!}>
    <h1 class="text-3xl font-bold">{{ $attributes['heading'] ?? '' }}</h1>
    <a href="{{ esc_url_raw($attributes['ctaUrl'] ?? '#') }}" class="btn">
        {{ $attributes['ctaText'] ?? '' }}
    </a>
</section>
```

- `{{ }}` escapes; use `{!! !!}` only for `get_block_wrapper_attributes()` and `$content`.
- For URLs use `esc_url_raw()` inside `{{ }}` — `esc_url()` would be encoded twice.
- The render file must stay inside the block directory, or the block renders nothing and a warning is logged.
- A plain `render.php` still works, with the same variables.

## Inner Blocks: `<InnerBlocks />`

Write `<InnerBlocks />` in the template where the inner blocks go (ACF-style). In the editor they are edited right there; on the page they replace the tag, inside a `div` with the tag's `class` (default `pollora-inner-blocks`) — the same `div` the editor renders.

```blade
<article {!! get_block_wrapper_attributes(['class' => 'card']) !!}>
    <h3 class="card__title">{{ $attributes['title'] ?? '' }}</h3>
    <InnerBlocks
        class="card__body"
        allowedBlocks="{{ json_encode(['core/paragraph', 'core/list']) }}"
        template="{{ json_encode([['core/paragraph', ['placeholder' => 'Write…']]]) }}"
        templateLock="false"
    />
</article>
```

- Options are Gutenberg's inner blocks options as attributes: `allowedBlocks`, `template`, `templateLock`, `orientation`, `defaultBlock`, `directInsert`… JSON for arrays/objects (write it with `{{ json_encode(...) }}`, which escapes quotes and `>`), `"true"`/`"false"` for booleans.
- One list per block: only the first tag gets the inner blocks; further tags are dropped.
- The inner blocks are saved in `post_content`; the rest of the block is not, so template changes reach existing blocks.
- In the preview, `$content` is empty and the tag stays for the editor; `<script>` and `on…` attributes are left out. Use `$isPreview` for editor-only hints.
- Requires Pollora 13.34 (the runtime above). Do not use `ServerSideRender` or a hand-written `InnerBlocks` edit for a Blade block with inner blocks.

## Registration

There is nothing to write. Pollora registers the blocks of every active theme, plugin and module on WordPress `init`: whatever holds a `resources/views/blocks` directory (or the deprecated `resources/blocks`).

- Do **not** create a `BlocksServiceProvider` or call `register_block_type()`: a provider boots after `init` over HTTP and not at all for REST requests, so its blocks would exist in WP-CLI only.
- A `BlocksServiceProvider` left by an older `pollora:make:block` is harmless and can be deleted.
- Assets resolve through the module's asset container: `theme`, `plugin.{slug}` or `module.{slug}`.

## Tailwind CSS in Blocks

### Frontend + Editor Styles (style.css)

```css
@import "tailwindcss" source(".");

.wp-block-my-theme-hero-banner {
    @apply relative py-24 px-8 rounded-xl overflow-hidden;
}
```

### Editor-Only Styles (editor.css)

```css
@reference "tailwindcss";

.wp-block-my-theme-hero-banner {
    @apply border-2 border-dashed min-h-[300px];
}
```

## Vite Configuration for Blocks

`pollora:make:block` writes this on first use:

```js
import { wordpressPlugin } from '@roots/vite-plugin';
import { globSync } from 'glob';

const blockEntries = globSync([
    './resources/views/blocks/*/{index,view}.{js,jsx,ts,tsx}',
    './resources/views/blocks/*/{editor,style}.css',
])
    .reduce((acc, file) => {
        acc[file.replace(/^\.\//, '').replace(/\.\w+$/, '')] = file;
        return acc;
    }, {});
const hasBlocks = Object.keys(blockEntries).length > 0;

// input: [..., ...Object.values(blockEntries)]
// plugins: [..., ...(hasBlocks ? [wordpressPlugin()] : [])]
// refresh: Blade files only, so block JSX hot-reloads:
//   [...refreshPaths.filter((p) => p !== 'resources/views/**'), 'resources/views/**/*.blade.php']
```

## Migrating from `resources/blocks`

The old directory is still registered, with a deprecation notice in the log, until Pollora v15.

1. `git mv resources/blocks resources/views/blocks`
2. Delete `app/Providers/BlocksServiceProvider.php` if present
3. Update the `vite.config.js` globs and full reloads (running `pollora:make:block` does it)
4. Optionally convert a static block: add `"render": "file:./render.blade.php"`, move the markup of `save.jsx` into it, set `save: window.pollora.blocks.save`

## Important Notes

- Block names follow the pattern `{namespace}/{block-slug}` (e.g., `my-theme/hero-banner`)
- Use `@import "tailwindcss" source(".")` in `style.css`, `@reference "tailwindcss"` in `editor.css`
- Custom `BlockRegistrarInterface` implementations: both methods take an optional `?string $basePath = null` (since v13.32)

Pollora Block Development

When to use this skill

Use this skill when creating, configuring, or customizing WordPress Gutenberg blocks within a Pollora theme, plugin or module.

Generating a Block

php artisan pollora:make:block hero-banner --theme

Options:

  • --theme[=NAME] — Create in a theme (default: the active theme)
  • --plugin=NAME — Create in a plugin
  • --static — A static block saved in post_content (save.jsx, no render.blade.php)
  • --inner-blocks — Add inner blocks: <InnerBlocks /> in render.blade.php (Gutenberg's InnerBlocks in edit.jsx/save.jsx with --static)
  • --namespace=NS, --title=TITLE, --category=CAT, --icon=ICON, --no-view-script, --force
  • --dynamic is deprecated: blocks are dynamic by default

On the first block, the command patches vite.config.js (block entries, wordpressPlugin(), Blade-only full reloads) and adds the npm dependencies. It refuses a theme or plugin with no package.json or no vite.config.js.

Block Structure

resources/views/blocks/hero-banner/
├── block.json           # WordPress block metadata
├── render.blade.php     # Server-side render (default)
├── index.jsx            # Entry point & registration
├── edit.jsx             # Editor component — shows what the page will show
├── save.jsx             # Static blocks only (--static)
├── editor.css           # Editor-only styles
├── style.css            # Shared frontend + editor styles
└── view.js              # Frontend-only script (optional)

Blocks are dynamic by default: render.blade.php renders them on each request, so their markup is not stored in post_content. Changing the markup updates every existing block instead of triggering "This block contains unexpected or invalid content".

block.json

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "my-theme/hero-banner",
    "version": "1.0.0",
    "title": "Hero Banner",
    "category": "theme",
    "icon": "cover-image",
    "supports": {
        "html": false,
        "align": ["wide", "full"]
    },
    "attributes": {
        "heading": { "type": "string", "default": "" },
        "ctaText": { "type": "string", "default": "Learn More" },
        "ctaUrl": { "type": "string", "default": "#" }
    },
    "textdomain": "my-theme",
    "editorScript": "file:./index.jsx",
    "editorStyle": "file:./editor.css",
    "style": "file:./style.css",
    "viewScript": "file:./view.js",
    "render": "file:./render.blade.php"
}

Editor Component (edit.jsx) and save

A generated dynamic block is edited through the framework's editor runtime, window.pollora.blocks, which Pollora loads with every block that has a render template (handle pollora-block-editor). Nothing to install or import:

// edit.jsx
import metadata from './block.json';

export default window.pollora.blocks.bladeEdit(metadata);

// index.jsx
registerBlockType(metadata.name, {
    edit: Edit,
    save: window.pollora.blocks.save, // stores the inner blocks, nothing else
});

bladeEdit previews render.blade.php through the core block-renderer REST route (again 200 ms after an attribute change) and makes its <InnerBlocks /> editable in place. Framework v13.32.0-beta.8 and earlier generate ServerSideRender and save: () => null instead — check edit.jsx before assuming.

Rendering with Blade (render.blade.php)

The template receives $attributes (array), $content (inner blocks HTML), $block (WP_Block) and $isPreview (true while it renders for the editor's preview). Blade components work as in any view.

<section {!! get_block_wrapper_attributes(['class' => 'py-16']) !!}>
    <h1 class="text-3xl font-bold">{{ $attributes['heading'] ?? '' }}</h1>
    <a href="{{ esc_url_raw($attributes['ctaUrl'] ?? '#') }}" class="btn">
        {{ $attributes['ctaText'] ?? '' }}
    </a>
</section>
  • {{ }} escapes; use {!! !!} only for get_block_wrapper_attributes() and $content.
  • For URLs use esc_url_raw() inside {{ }} — esc_url() would be encoded twice.
  • The render file must stay inside the block directory, or the block renders nothing and a warning is logged.
  • A plain render.php still works, with the same variables.

Inner Blocks: <InnerBlocks />

Write <InnerBlocks /> in the template where the inner blocks go (ACF-style). In the editor they are edited right there; on the page they replace the tag, inside a div with the tag's class (default pollora-inner-blocks) — the same div the editor renders.

<article {!! get_block_wrapper_attributes(['class' => 'card']) !!}>
    <h3 class="card__title">{{ $attributes['title'] ?? '' }}</h3>
    <InnerBlocks
        class="card__body"
        allowedBlocks="{{ json_encode(['core/paragraph', 'core/list']) }}"
        template="{{ json_encode([['core/paragraph', ['placeholder' => 'Write…']]]) }}"
        templateLock="false"
    />
</article>
  • Options are Gutenberg's inner blocks options as attributes: allowedBlocks, template, templateLock, orientation, defaultBlock, directInsert… JSON for arrays/objects (write it with {{ json_encode(...) }}, which escapes quotes and >), "true"/"false" for booleans.
  • One list per block: only the first tag gets the inner blocks; further tags are dropped.
  • The inner blocks are saved in post_content; the rest of the block is not, so template changes reach existing blocks.
  • In the preview, $content is empty and the tag stays for the editor; <script> and on… attributes are left out. Use $isPreview for editor-only hints.
  • Requires Pollora 13.34 (the runtime above). Do not use ServerSideRender or a hand-written InnerBlocks edit for a Blade block with inner blocks.

Registration

There is nothing to write. Pollora registers the blocks of every active theme, plugin and module on WordPress init: whatever holds a resources/views/blocks directory (or the deprecated resources/blocks).

  • Do not create a BlocksServiceProvider or call register_block_type(): a provider boots after init over HTTP and not at all for REST requests, so its blocks would exist in WP-CLI only.
  • A BlocksServiceProvider left by an older pollora:make:block is harmless and can be deleted.
  • Assets resolve through the module's asset container: theme, plugin.{slug} or module.{slug}.

Tailwind CSS in Blocks

Frontend + Editor Styles (style.css)

@import "tailwindcss" source(".");

.wp-block-my-theme-hero-banner {
    @apply relative py-24 px-8 rounded-xl overflow-hidden;
}

Editor-Only Styles (editor.css)

@reference "tailwindcss";

.wp-block-my-theme-hero-banner {
    @apply border-2 border-dashed min-h-[300px];
}

Vite Configuration for Blocks

pollora:make:block writes this on first use:

import { wordpressPlugin } from '@roots/vite-plugin';
import { globSync } from 'glob';

const blockEntries = globSync([
    './resources/views/blocks/*/{index,view}.{js,jsx,ts,tsx}',
    './resources/views/blocks/*/{editor,style}.css',
])
    .reduce((acc, file) => {
        acc[file.replace(/^\.\//, '').replace(/\.\w+$/, '')] = file;
        return acc;
    }, {});
const hasBlocks = Object.keys(blockEntries).length > 0;

// input: [..., ...Object.values(blockEntries)]
// plugins: [..., ...(hasBlocks ? [wordpressPlugin()] : [])]
// refresh: Blade files only, so block JSX hot-reloads:
//   [...refreshPaths.filter((p) => p !== 'resources/views/**'), 'resources/views/**/*.blade.php']

Migrating from resources/blocks

The old directory is still registered, with a deprecation notice in the log, until Pollora v15.

  1. git mv resources/blocks resources/views/blocks
  2. Delete app/Providers/BlocksServiceProvider.php if present
  3. Update the vite.config.js globs and full reloads (running pollora:make:block does it)
  4. Optionally convert a static block: add "render": "file:./render.blade.php", move the markup of save.jsx into it, set save: window.pollora.blocks.save

Important Notes

  • Block names follow the pattern {namespace}/{block-slug} (e.g., my-theme/hero-banner)
  • Use @import "tailwindcss" source(".") in style.css, @reference "tailwindcss" in editor.css
  • Custom BlockRegistrarInterface implementations: both methods take an optional ?string $basePath = null (since v13.32)
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core