From 04309c987e41191644a531d98c9e4b006a9bb294 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Mon, 28 Sep 2026 10:06:22 +0200 Subject: [PATCH 1/4] fix: list the blocks of a theme made since v13.32 active_theme_info read resources/blocks only, where themes no longer keep their blocks: it reported none. It reads resources/views/blocks, counts a directory only when it holds a block.json, and reports the blocks left in the deprecated resources/blocks separately. --- src/Mcp/Tools/ActiveThemeInfo.php | 29 +++++++++++++++++---- tests/Feature/Tools/ActiveThemeInfoTest.php | 22 +++++++++++++--- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/Mcp/Tools/ActiveThemeInfo.php b/src/Mcp/Tools/ActiveThemeInfo.php index 297f28d..29d4248 100644 --- a/src/Mcp/Tools/ActiveThemeInfo.php +++ b/src/Mcp/Tools/ActiveThemeInfo.php @@ -37,11 +37,8 @@ public function handle(Request $request): Response $providers = $this->findProviders($themePath); - $blocks = File::isDirectory("{$themePath}/resources/blocks") - ? collect(File::directories("{$themePath}/resources/blocks")) - ->map(fn (string $dir): string => basename($dir)) - ->all() - : []; + $blocks = $this->findBlocks("{$themePath}/resources/views/blocks"); + $deprecatedBlocks = $this->findBlocks("{$themePath}/resources/blocks"); $views = File::isDirectory("{$themePath}/resources/views") ? $this->listBladeTemplates("{$themePath}/resources/views") @@ -60,6 +57,7 @@ public function handle(Request $request): Response 'config_files' => $configFiles, 'service_providers' => $providers, 'blocks' => $blocks, + 'blocks_in_deprecated_directory' => $deprecatedBlocks, 'blade_templates' => $views, 'has_vite' => $hasVite, 'has_package_json' => $hasPackageJson, @@ -69,6 +67,27 @@ public function handle(Request $request): Response ]); } + /** + * Block slugs in a directory: each subdirectory holding a block.json. + * + * Blocks live in resources/views/blocks; resources/blocks is still + * registered by Pollora, with a deprecation notice, until v15. + * + * @return array + */ + private function findBlocks(string $directory): array + { + if (! File::isDirectory($directory)) { + return []; + } + + return collect(File::directories($directory)) + ->filter(fn (string $dir): bool => File::exists("{$dir}/block.json")) + ->map(fn (string $dir): string => basename($dir)) + ->values() + ->all(); + } + /** * @return array */ diff --git a/tests/Feature/Tools/ActiveThemeInfoTest.php b/tests/Feature/Tools/ActiveThemeInfoTest.php index 6d0405a..125e569 100644 --- a/tests/Feature/Tools/ActiveThemeInfoTest.php +++ b/tests/Feature/Tools/ActiveThemeInfoTest.php @@ -11,7 +11,9 @@ // Create theme directory structure @mkdir("{$themePath}/config", 0777, true); @mkdir("{$themePath}/app/Providers", 0777, true); - @mkdir("{$themePath}/resources/blocks/hero", 0777, true); + @mkdir("{$themePath}/resources/views/blocks/hero", 0777, true); + @mkdir("{$themePath}/resources/views/blocks/not-a-block", 0777, true); + @mkdir("{$themePath}/resources/blocks/legacy-banner", 0777, true); @mkdir("{$themePath}/resources/views/partials", 0777, true); file_put_contents("{$themePath}/config/menus.php", 'assertSee('ThemeServiceProvider'); }); -it('lists blocks', function () { +it('lists blocks from resources/views/blocks', function () { Nectar::tool(ActiveThemeInfo::class) ->assertOk() - ->assertSee('hero'); + ->assertSee('"blocks":["hero"]'); +}); + +it('ignores a directory without block.json', function () { + Nectar::tool(ActiveThemeInfo::class) + ->assertOk() + ->assertDontSee('not-a-block'); +}); + +it('reports blocks left in the deprecated resources/blocks', function () { + Nectar::tool(ActiveThemeInfo::class) + ->assertOk() + ->assertSee('"blocks_in_deprecated_directory":["legacy-banner"]'); }); it('lists blade templates recursively', function () { From 99ebcda23002b8366e05d4e84024d18a38915626 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Mon, 28 Sep 2026 10:06:22 +0200 Subject: [PATCH 2/4] docs: bring the guidelines and skills up to framework v13.32 The guidelines listed the Loop and Query facades, removed from the framework, and the blocks skill taught resources/blocks, render.php and a hand-written BlocksServiceProvider. They now teach what v13.32 does: blocks in resources/views/blocks rendered with Blade and registered by Pollora, the facades that exist, __() routing, pollora_register(), #[Ajax], #[Ability], #[SkipDiscovery], the login screen, the template marker, the full list of commands, and the 13.x requirements. A pollora-abilities skill covers the WordPress Abilities API. --- README.md | 12 +- resources/boost/guidelines/13/core.blade.php | 17 ++- resources/boost/guidelines/core.blade.php | 41 ++++-- .../boost/skills/pollora-abilities/SKILL.md | 83 +++++++++++ .../boost/skills/pollora-blocks/SKILL.md | 133 +++++++++--------- .../boost/skills/pollora-post-types/SKILL.md | 2 +- .../boost/skills/pollora-rest-api/SKILL.md | 29 +++- .../boost/skills/pollora-theming/SKILL.md | 36 ++++- 8 files changed, 266 insertions(+), 87 deletions(-) create mode 100644 resources/boost/skills/pollora-abilities/SKILL.md diff --git a/README.md b/README.md index 33d6d1a..79d02b9 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Nectar feeds AI agents with the context they need to write high-quality Pollora | Feature | Description | |---------|-------------| | **AI Guidelines** | Pollora architecture, PHP attributes, WordPress routing, Blade theming — injected into your agent's context via Boost | -| **8 Agent Skills** | On-demand knowledge for post types, taxonomies, theming, hooks, blocks, REST API, scheduling, and modules | +| **9 Agent Skills** | On-demand knowledge for post types, taxonomies, theming, hooks, blocks, REST API and AJAX, scheduling, modules, and abilities | | **10 MCP Tools** | Live introspection of your WordPress & Pollora environment directly from your AI agent | | **Upgrade Prompts** | Step-by-step MCP prompts for upgrading between Pollora major versions (e.g., 12→13) | @@ -112,18 +112,20 @@ Skills are activated on-demand when working on specific tasks: | `pollora-taxonomies` | Creating custom taxonomies | | `pollora-theming` | Theme development (Blade, Vite, Tailwind, assets) | | `pollora-hooks` | Registering WordPress actions & filters | -| `pollora-blocks` | Gutenberg block development with JSX & Tailwind | -| `pollora-rest-api` | REST API endpoints with `#[WpRestRoute]` | +| `pollora-blocks` | Gutenberg blocks with JSX, Blade rendering & Tailwind | +| `pollora-rest-api` | REST API endpoints with `#[WpRestRoute]`, AJAX handlers with `#[Ajax]` | | `pollora-scheduling` | Scheduled tasks with `#[Schedule]` | +| `pollora-abilities` | WordPress Abilities API with `#[Ability]` and the `Ability` facade | | `pollora-modules` | Laravel Modules with auto-discovery | ## Upgrade Assistance -Nectar provides MCP upgrade prompts that guide AI agents through Pollora major version upgrades. Prompts are **automatically registered** when the current project version matches. +Nectar provides MCP upgrade prompts that guide AI agents through Pollora version upgrades. Prompts are **automatically registered** when the current project version matches. | Prompt | Available when | Covers | |--------|---------------|--------| | `upgrade-pollora-v13` | Pollora 12.x detected | Loop facade removal, config registration removal, CSRF middleware rename, Vite theme.json setup, WordPress 7.0, and more | +| `upgrade-pollora-v13-32` | Pollora 13.0 to 13.4.x detected | Dependencies and composer-patches 2, renamed commands, extracted Hook/Option/Ajax packages, Loop/Query removal, blocks in `resources/views/blocks`, skeleton files (`routes/web.php`, cache table, `.htaccess`), theme.json from `@theme` | The upgrade prompt includes: - Step-by-step process (assess → safety net → analyze → apply → update deps → clean up) @@ -157,7 +159,7 @@ return [ - PHP ^8.3 - Laravel 12.x or 13.x -- Pollora Framework (any version) +- Pollora Framework 12.x or 13.x - Laravel Boost ^2.0 ## License diff --git a/resources/boost/guidelines/13/core.blade.php b/resources/boost/guidelines/13/core.blade.php index fa2c425..b028cb7 100644 --- a/resources/boost/guidelines/13/core.blade.php +++ b/resources/boost/guidelines/13/core.blade.php @@ -1,20 +1,25 @@ ## Pollora 13.x Specifics -Pollora 13.x targets **Laravel 13.x** and **PHP 8.3+**. +Pollora 13.x targets **Laravel 13.x** and **PHP 8.3+**. Since v13.32 the framework version tracks the Laravel release it targets. ### Laravel 13 Compatibility -- Uses `illuminate/*` packages at `^13.0` +- Requires `illuminate/*` `^13.32` - Supports Pest 3.x for testing - PHPStan level 5 with WordPress and Laravel extensions - Rector with Laravel-specific rules -### WordPress 6.9 Integration +### WordPress 7 Integration -- WordPress installed at `public/cms/` +- WordPress 7.x (`johnpbloch/wordpress` `^7.0`) installed at `public/cms/` - Content directory at `public/content/` -- Full Site Editing support via `theme.json` -- Gutenberg blocks with Vite + JSX/TSX + Tailwind CSS v4 +- Full Site Editing support via `theme.json`, generated from the theme's `@theme` tokens +- Gutenberg blocks with Vite + JSX/TSX + Tailwind CSS v4, rendered with Blade +- Abilities API through `pollora/abilities` (WordPress 6.9+) + +### Extracted Packages + +Some modules now live in their own packages, installed with the framework: `pollora/hook` (hook domain and adapters), `pollora/option`, `pollora/ajax`, `pollora/abilities`. ### Discovery System Enhancements diff --git a/resources/boost/guidelines/core.blade.php b/resources/boost/guidelines/core.blade.php index 6dc6dd8..b288700 100644 --- a/resources/boost/guidelines/core.blade.php +++ b/resources/boost/guidelines/core.blade.php @@ -60,6 +60,18 @@ class ItemAPI {} // Scheduled tasks #[Schedule(Every::DAY)] public function dailyCleanup(): void {} + +// AJAX handlers — logged-in users only unless access says otherwise +#[Ajax('load_more', access: AjaxAccess::ALL)] +public function loadMore(): void {} + +// WordPress Abilities API (WP 6.9+) — a class implementing AbilityHandler +#[Ability(name: 'acme/create-post', description: 'Creates a post.', category: 'acme-content')] +final class CreatePost implements AbilityHandler {} + +// Keep a class out of discovery entirely (or all but some: except: [...]) +#[SkipDiscovery] +class InternalHelper {} @endverbatim @@ -74,33 +86,44 @@ public function dailyCleanup(): void {} ├── resources/ │ ├── assets/ # JS, CSS, fonts, images │ └── views/ # Blade templates -├── functions.php # Theme registration entry point +│ └── blocks/ # Gutenberg blocks, registered automatically +├── functions.php # pollora_register(ModuleType::Theme) ├── style.css # WordPress theme metadata -├── theme.json # Block editor settings +├── theme.json # Base editor settings; the build adds the @theme tokens └── vite.config.js # Vite build config ``` -Themes use Vite for asset bundling with HMR, Tailwind CSS v4, and the `Asset` facade for script/style registration. Theme classes use the `Theme\{ThemeName}\` namespace. +Themes use Vite for asset bundling with HMR, Tailwind CSS v4, and the `Asset` facade for script/style registration. Theme classes use the `Theme\{ThemeName}\` namespace. A plugin registers with `pollora_register(ModuleType::Plugin, 'my-plugin', __DIR__)` (`use Pollora\Modules\Domain\Enums\ModuleType;`). + +Design tokens (colours, font sizes, fonts, radii) live in the `@theme static` block of `app.css` with concrete values; the build writes them into the `theme.json` the editor reads. `get_theme_file_uri()` returns the built URL of a theme asset. ### Key Conventions - **Never call WordPress registration functions directly** — use attributes and discovery - **Use Blade directives** from Sage Directives: `@posts`, `@title`, `@content`, `@permalink`, `@published` - **WordPress objects** (`WP_Post`, `WP_Query`, `WP_User`) are auto-injected via type hints in controller methods -- **Facades**: `Action`, `Filter`, `Query`, `Asset`, `Theme`, `PostType`, `Taxonomy`, `Option`, `Loop`, `Ajax` +- **Facades**: `Action`, `Filter`, `Ajax`, `Asset`, `Theme`, `PostType`, `Taxonomy`, `Option`, `Ability`, `PostQuery`, `MetaQuery`, `TaxQuery`, `Mail`, `Constant`. There is no `Loop` or `Query` facade: use Sage Directives in Blade, WordPress functions in PHP +- **Translations**: `__('Text', 'my-domain')` goes to WordPress's catalogues; `__('key', ['name' => $x])` goes to Laravel; `__('Text')` tries Laravel, then WordPress's `default` domain +- **Blocks**: live in `resources/views/blocks/{slug}`, render with `render.blade.php` by default, and are registered by Pollora — never write a `BlocksServiceProvider` +- **Which template answered?** With `WP_DEBUG` on, every page carries `` (hierarchy responses only, not `Route::wp()` or Laravel routes) - **CSRF**: WordPress endpoints are excluded from Laravel CSRF — WordPress uses its own nonce system - **Modules**: Use `nwidart/laravel-modules` for large projects — discovery works inside modules automatically ### Available Artisan Commands -- `pollora:install` — Full project installation -- `pollora:make:theme` — Generate a new theme -- `pollora:make:block` — Generate a Gutenberg block -- `pollora:make:post-type` — Generate a post type class -- `pollora:make:action` / `pollora:make:filter` — Generate hook classes +- `pollora:install` — Full project installation (`--theme` to pick the generated theme; runs without interaction) +- `pollora:env:setup` — Install and configure WordPress +- `pollora:make:theme` / `pollora:theme:delete` / `pollora:theme:status` — Themes +- `pollora:make:plugin` / `pollora:plugin:list` / `pollora:plugin:status` — Plugins +- `pollora:make:block` — Generate a Gutenberg block (dynamic Blade by default, `--static` for `save.jsx`) +- `pollora:make:post-type` / `pollora:make:taxonomy` — Generate post type and taxonomy classes +- `pollora:make:action` / `pollora:make:filter` / `pollora:make:hook` — Generate hook classes +- `pollora:make:model` / `pollora:make:wp-cli` — Generate an Eloquent model or a WP-CLI command class - `discovery:run` / `discovery:clear` — Manage component discovery cache - `pollora:status` — Show framework status +Commands use the colon convention since v13.32; the former dashed names (`pollora:make-theme`…) still work as aliases. + ### Pollora Nectar MCP Tools When available, use the `pollora-nectar` MCP server for live introspection: diff --git a/resources/boost/skills/pollora-abilities/SKILL.md b/resources/boost/skills/pollora-abilities/SKILL.md new file mode 100644 index 0000000..7122948 --- /dev/null +++ b/resources/boost/skills/pollora-abilities/SKILL.md @@ -0,0 +1,83 @@ +--- +name: pollora-abilities +description: Declare WordPress Abilities API abilities (WordPress 6.9+) with the Pollora Ability facade or the #[Ability] attribute, so AI agents and automation tools can discover and invoke site features. +--- + +# Pollora Abilities + +## When to use this skill +Use this skill when exposing site functionality to AI agents or automation tools through the WordPress Abilities API (WordPress 6.9+). Pollora wraps it in `pollora/abilities`, installed with the framework. On an older WordPress, declarations are accepted and never published. + +An ability is not an MCP tool: the MCP Adapter plugin publishes abilities over MCP, and core exposes them at `/wp-json/wp-abilities/v1/abilities`. Declare the ability once; consumers pick it up. + +## Categories + +Every ability belongs to a category that must exist. Declare it once, typically in a service provider. Slugs are global to the install and core claims several: prefix yours. + +```php +use Pollora\Support\Facades\Ability; + +Ability::category('acme-content', 'Editorial', 'Posts and pages.'); +``` + +## Declarative API (preferred for anything non-trivial) + +```php +use Pollora\Abilities\Domain\Contracts\AbilityHandler; +use Pollora\Abilities\Domain\Model\Behaviour; +use Pollora\Abilities\Domain\Model\Input; +use Pollora\Abilities\Domain\Schema\SchemaBuilder; +use Pollora\Attributes\Ability; + +#[Ability( + name: 'acme/create-post', + description: 'Creates a post from a title and a status.', + category: 'acme-content', + behaviour: Behaviour::Creates, +)] +final class CreatePost implements AbilityHandler +{ + public function schema(SchemaBuilder $schema): void + { + $schema->string('title', 'Title of the post to create.', required: true); + $schema->enum('status', 'Publication status.', ['draft', 'publish'], default: 'draft'); + } + + public function authorize(Input $input): mixed + { + return current_user_can('edit_posts') + ?: new WP_Error('forbidden', 'You cannot create posts.', ['status' => 403]); + } + + public function handle(Input $input): mixed + { + return ['id' => wp_insert_post([ + 'post_title' => $input->string('title'), + 'post_status' => $input->string('status', 'draft'), + ])]; + } +} +``` + +The class is discovered anywhere discovery scans and built through the container. A class with `#[Ability]` that does not implement `AbilityHandler` is logged as an error. A missing category is declared for you. + +## Imperative API + +```php +Ability::define('acme/get-posts') + ->description('Returns the most recent posts, newest first.') + ->category('acme-content') + ->input(fn (SchemaBuilder $schema) => $schema + ->integer('limit', 'How many posts to return.', default: 10, minimum: 1, maximum: 100)) + ->can(fn (Input $input): bool => current_user_can('edit_posts')) + ->using(fn (Input $input): array => /* ... */ []); +``` + +Nothing is registered until `using()` or `handledBy()` supplies a body. Pollora queues declarations and publishes them on WordPress's abilities init hooks — never hook those yourself. + +## Rules + +- **Names** are `namespace/slug`, lowercase alphanumerics and single dashes; a bare slug, an empty label or description is refused at declaration. +- **Permissions default to refusing**: without `can()` / `authorize()` the ability refuses everything. The check receives the same input as the body — check `edit_post` on the given id rather than a blanket `edit_posts`. Return a `WP_Error` to explain a refusal. +- **Behaviour** (advisory hints for clients): `Reads` (default), `Creates`, `Updates`, `Deletes` — facade `->reads()`, `->creates()`, `->updates()`, `->deletes()`. Declare it truthfully; the permission check is what protects the site. +- **Input** is a defensive reader: `$input->string()`, `integer(default:, max:)`, `float()`, `id()`, `boolean()`, `stringList()` never throw on a missing or malformed value. diff --git a/resources/boost/skills/pollora-blocks/SKILL.md b/resources/boost/skills/pollora-blocks/SKILL.md index 6655261..b7b45b6 100644 --- a/resources/boost/skills/pollora-blocks/SKILL.md +++ b/resources/boost/skills/pollora-blocks/SKILL.md @@ -1,37 +1,45 @@ --- name: pollora-blocks -description: Create Gutenberg blocks in Pollora themes with Vite, JSX/TSX, and Tailwind CSS integration. +description: Create Gutenberg blocks in Pollora themes, plugins and modules with Vite, JSX/TSX, Blade rendering and Tailwind CSS. --- # Pollora Block Development ## When to use this skill -Use this skill when creating, configuring, or customizing WordPress Gutenberg blocks within a Pollora theme. +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 --dynamic +php artisan pollora:make:block hero-banner --theme ``` Options: -- `--theme` — Create in the active theme's `resources/blocks/` directory -- `--dynamic` — Include a `render.php` for server-side rendering +- `--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 InnerBlocks support +- `--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/blocks/hero-banner/ +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 (what authors see) -├── save.jsx # Static save (or null for dynamic blocks) -├── render.php # Server-side render (dynamic blocks only) +├── 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 interactive script +└── 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 @@ -43,7 +51,6 @@ resources/blocks/hero-banner/ "title": "Hero Banner", "category": "theme", "icon": "cover-image", - "description": "A full-width hero banner with heading and CTA.", "supports": { "html": false, "align": ["wide", "full"] @@ -58,70 +65,57 @@ resources/blocks/hero-banner/ "editorStyle": "file:./editor.css", "style": "file:./style.css", "viewScript": "file:./view.js", - "render": "file:./render.php" + "render": "file:./render.blade.php" } ``` ## Editor Component (edit.jsx) +For a dynamic block, the editor should show what the page shows: render the same markup (or use `ServerSideRender`). + ```jsx import { useBlockProps, RichText } from '@wordpress/block-editor'; -import { TextControl } from '@wordpress/components'; export default function Edit({ attributes, setAttributes }) { - const blockProps = useBlockProps(); - return ( -
+
setAttributes({ heading })} placeholder="Enter heading..." /> - setAttributes({ ctaText })} - /> -
+ ); } ``` -## Dynamic Render (render.php) - -```php - -
> -

- - +## Rendering with Blade (render.blade.php) + +The template receives `$attributes` (array), `$content` (inner blocks HTML) and `$block` (`WP_Block`). Blade components work as in any view. + +```blade +
'py-16']) !!}> +

{{ $attributes['heading'] ?? '' }}

+
+ {{ $attributes['ctaText'] ?? '' }} -
+ {!! $content !!} + ``` -## Registering Blocks +- `{{ }}` 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. -In a service provider: +## Registration -```php -use Pollora\Block\Infrastructure\Services\BlockRegistrar; +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`). -public function boot(BlockRegistrar $registrar): void -{ - $registrar->registerDirectory( - directory: dirname(__DIR__, 2) . '/resources/blocks', - containerName: 'theme', - ); -} -``` +- 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 @@ -132,7 +126,6 @@ public function boot(BlockRegistrar $registrar): void .wp-block-my-theme-hero-banner { @apply relative py-24 px-8 rounded-xl overflow-hidden; - background: linear-gradient(135deg, theme(--color-indigo-950) 0%, theme(--color-violet-900) 100%); } ``` @@ -148,25 +141,39 @@ public function boot(BlockRegistrar $registrar): void ## Vite Configuration for Blocks +`pollora:make:block` writes this on first use: + ```js -import { globSync } from 'fs'; -import path from 'path'; +import { wordpressPlugin } from '@roots/vite-plugin'; +import { globSync } from 'glob'; -const blockEntries = globSync('./resources/blocks/*/{index,view}.{js,jsx,ts,tsx}') - .concat(globSync('./resources/blocks/*/{editor,style}.css')) +const blockEntries = globSync([ + './resources/views/blocks/*/{index,view}.{js,jsx,ts,tsx}', + './resources/views/blocks/*/{editor,style}.css', +]) .reduce((acc, file) => { - const slug = path.basename(path.dirname(file)); - const name = path.basename(file, path.extname(file)); - acc[`blocks/${slug}/${name}`] = 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: () => null` + ## Important Notes -- Block names follow the pattern `{theme-slug}/{block-slug}` (e.g., `my-theme/hero-banner`) -- Use `@import "tailwindcss" source(".")` in `style.css` for full Tailwind support -- Use `@reference "tailwindcss"` in `editor.css` for editor-only Tailwind utilities -- Dynamic blocks use `render.php` and return `null` from `save.jsx` -- Vite auto-discovers block assets — no manual entry configuration needed -- The `BlockRegistrar` handles WordPress `register_block_type()` automatically \ No newline at end of file +- 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) diff --git a/resources/boost/skills/pollora-post-types/SKILL.md b/resources/boost/skills/pollora-post-types/SKILL.md index 3c568dc..d278c44 100644 --- a/resources/boost/skills/pollora-post-types/SKILL.md +++ b/resources/boost/skills/pollora-post-types/SKILL.md @@ -123,4 +123,4 @@ public function show(\WP_Post $post): \Illuminate\View\View - Run `php artisan discovery:clear` after creating a new post type class during development - Labels are auto-generated from the class name if not specified - The slug is derived from the `#[PostType('slug')]` attribute parameter -- Generate a post type with `php artisan pollora:make-post-type` \ No newline at end of file +- Generate a post type with `php artisan pollora:make:post-type` \ No newline at end of file diff --git a/resources/boost/skills/pollora-rest-api/SKILL.md b/resources/boost/skills/pollora-rest-api/SKILL.md index 2293a0d..641fe7d 100644 --- a/resources/boost/skills/pollora-rest-api/SKILL.md +++ b/resources/boost/skills/pollora-rest-api/SKILL.md @@ -1,6 +1,6 @@ --- name: pollora-rest-api -description: Build WordPress REST API endpoints using Pollora WpRestRoute attributes with automatic discovery and permission management. +description: Build WordPress REST API endpoints and AJAX handlers using Pollora WpRestRoute and Ajax attributes with automatic discovery and permission management. --- # Pollora REST API Development @@ -146,6 +146,33 @@ Endpoint: `GET /api/products/search` 'api_plugins' => ['*'], // All plugins ``` +## AJAX Handlers (admin-ajax.php) + +For `admin-ajax.php` actions, put `#[Ajax]` on a public method. The class is discovered and built through the container. Handlers are **logged-in users only** unless `access` says otherwise. + +```php +use Pollora\Attributes\Ajax; +use Pollora\Ajax\Domain\Model\AjaxAccess; + +class NewsletterHandler +{ + #[Ajax('subscribe')] // wp_ajax_subscribe (logged in) + public function subscribe(): void + { + check_ajax_referer('newsletter'); + wp_send_json_success(['message' => 'Subscribed!']); + } + + #[Ajax('load_more', access: AjaxAccess::ALL)] // + wp_ajax_nopriv_load_more + public function loadMore(): void + { + wp_send_json_success([]); + } +} +``` + +`AjaxAccess::LOGGED` (default), `AjaxAccess::ALL`, `AjaxAccess::GUEST`. The imperative form is `Ajax::listen('action', $handler)->forAllUsers()` / `->forGuestUsers()`. + ## Choosing Between Approaches | Feature | WpRestRoute Attributes | Theme API Routes | diff --git a/resources/boost/skills/pollora-theming/SKILL.md b/resources/boost/skills/pollora-theming/SKILL.md index 0e29acc..9a2252b 100644 --- a/resources/boost/skills/pollora-theming/SKILL.md +++ b/resources/boost/skills/pollora-theming/SKILL.md @@ -29,6 +29,7 @@ themes/my-theme/ ├── config/ │ ├── gutenberg.php # Block editor settings │ ├── images.php # Custom image sizes +│ ├── login.php # Login screen (opt-in), see below │ ├── menus.php # Menu locations │ ├── providers.php # Additional service providers │ ├── sidebars.php # Widget areas @@ -43,16 +44,16 @@ themes/my-theme/ │ └── views/ │ ├── layouts/ │ │ └── app.blade.php # Main layout +│ ├── blocks/ # Gutenberg blocks (see pollora-blocks) │ ├── parts/ # Reusable partials │ ├── home.blade.php │ ├── page.blade.php │ ├── single.blade.php │ └── index.blade.php -├── functions.php # Theme registration entry point +├── functions.php # pollora_register(ModuleType::Theme) ├── style.css # WordPress theme metadata (name, version, description) ├── theme.json # Base block editor config; the build adds the @theme tokens ├── vite.config.js # Vite build configuration -├── tailwind.config.js # Tailwind (v3) or not needed (v4 auto-detection) └── package.json ``` @@ -96,6 +97,8 @@ $logoUrl = Asset::url('assets/images/logo.png'); $cssUrl = Asset::url('assets/css/app.css')->from('theme'); ``` +WordPress's `get_theme_file_uri('resources/assets/images/logo.svg')` also returns the URL the build gave that file, so plugins and core code that call it work. A theme file outside the Vite build has no public URL. + ### Vite Configuration ```js @@ -178,6 +181,35 @@ return [ ]; ``` +## Login Screen + +Add `config/login.php` and the WordPress login screen wears the theme's design, read from `theme.json` (colours, radii, fonts). Without the file, WordPress's screen is unchanged. + +```php +return [ + 'enabled' => true, + 'logo' => [ + 'source' => 'resources/assets/images/logo.svg', // path in the theme, URL or attachment id + 'width' => 220, + ], + // Only when the palette uses its own slug names: + // 'tokens' => ['primary' => ['brand-600'], 'accent' => 'oklch(70% .2 30)'], +]; +``` + +Roles resolve from common slugs (`primary`, `surface`, `foreground`, `outline`…). Modules and plugins can take over with the `pollora/login/palette`, `pollora/login/logo`, `pollora/login/styles` and `pollora/login/credit` filters. + +## Debugging Templates + +With `WP_DEBUG` on, each page rendered through the template hierarchy says which template answered: + +```bash +curl -s https://site.test/some-page | grep pollora:template +# +``` + +Responses from `Route::wp()` and Laravel routes carry no marker. + ## Important Notes - **Never create WordPress PHP template files** — use Blade exclusively From 6c16a0fd8ce47aa59ffdd490459088065c56189f Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Mon, 28 Sep 2026 10:06:22 +0200 Subject: [PATCH 3/4] feat: add an upgrade prompt from Pollora 13.x to 13.32 No prompt covered the move from 13.4 to 13.32, whose breaking changes are many: composer-patches 2, renamed commands, classes moved to extracted packages, Loop and Query removed, blocks moved, skeleton files changed. The upgrade-pollora-v13-32 prompt walks through them, registered when 13.0 to 13.4.x is installed. The 12 to 13 prompt no longer adds a copy-theme-json build step: copying the built theme.json over the theme's own froze its palette. --- src/Mcp/Nectar.php | 2 + .../upgrade-pollora-v13.blade.php | 23 +- .../UpgradePolloraV1332.php | 58 +++++ .../upgrade-pollora-v13-32.blade.php | 217 ++++++++++++++++++ .../Prompts/UpgradePolloraV1332Test.php | 25 ++ 5 files changed, 309 insertions(+), 16 deletions(-) create mode 100644 src/Mcp/Prompts/UpgradePolloraV1332/UpgradePolloraV1332.php create mode 100644 src/Mcp/Prompts/UpgradePolloraV1332/upgrade-pollora-v13-32.blade.php create mode 100644 tests/Feature/Prompts/UpgradePolloraV1332Test.php diff --git a/src/Mcp/Nectar.php b/src/Mcp/Nectar.php index e575807..ad523b3 100644 --- a/src/Mcp/Nectar.php +++ b/src/Mcp/Nectar.php @@ -6,6 +6,7 @@ use Laravel\Mcp\Server; use Pollora\Nectar\Mcp\Prompts\UpgradePolloraV13\UpgradePolloraV13; +use Pollora\Nectar\Mcp\Prompts\UpgradePolloraV1332\UpgradePolloraV1332; use Pollora\Nectar\Mcp\Tools\ActiveThemeInfo; use Pollora\Nectar\Mcp\Tools\DiscoveredComponents; use Pollora\Nectar\Mcp\Tools\ModulesInfo; @@ -40,5 +41,6 @@ class Nectar extends Server protected array $prompts = [ UpgradePolloraV13::class, + UpgradePolloraV1332::class, ]; } diff --git a/src/Mcp/Prompts/UpgradePolloraV13/upgrade-pollora-v13.blade.php b/src/Mcp/Prompts/UpgradePolloraV13/upgrade-pollora-v13.blade.php index 505c136..a85d3a0 100644 --- a/src/Mcp/Prompts/UpgradePolloraV13/upgrade-pollora-v13.blade.php +++ b/src/Mcp/Prompts/UpgradePolloraV13/upgrade-pollora-v13.blade.php @@ -358,10 +358,10 @@ class BookGenre {} Generate blocks with the new Artisan command: ```bash -php artisan pollora:make:block my-block +php artisan pollora:make:block my-block --theme ``` -Creates a block with `block.json`, JSX/TSX entry, and CSS — built with Vite and Tailwind CSS v4. +Creates a block in `resources/views/blocks/my-block` with `block.json`, a JSX entry, CSS and a `render.blade.php` (dynamic by default since v13.32; `--static` for `save.jsx`) — built with Vite and Tailwind CSS v4, registered by Pollora without a service provider. ### Admin Dashboard (v13.4) @@ -435,7 +435,7 @@ public function handle(PostPublished $event): void cd themes/your-theme && npm install -D @roots/vite-plugin ``` -2. **Update `vite.config.js`** to add the `wordpressThemeJson` and `copy-theme-json` plugins: +2. **Update `vite.config.js`** to add the `wordpressThemeJson` plugin: @boostsnippet('Vite Config Update', 'js') // Add import @@ -445,21 +445,12 @@ public function handle(PostPublished $event): void wordpressThemeJson({ baseThemeJsonPath: './theme.json', }), -{ - name: "copy-theme-json", - apply: "build", - async writeBundle(options) { - const fs = await import('fs/promises'); - const src = path.join(options.dir, 'assets', 'theme.json'); - const dest = path.resolve(__dirname, 'theme.json'); - try { - await fs.copyFile(src, dest); - console.log(' ✓ theme.json copied to theme root'); - } catch {} - }, -}, @endboostsnippet +Do **not** copy the built `theme.json` back over the theme's own (a `copy-theme-json` step in `writeBundle`). The theme's `theme.json` is the base, and it wins over `@theme` on any slug it defines: once it holds the generated palette, later changes to `app.css` never reach the editor. If a project already has that step, remove it and strip the generated `palette`, `fontSizes`, `fontFamilies` and `radiusSizes` from the base. + +The editor's palette, font sizes, fonts and radii come from the `@theme` block of `app.css`. Declare them in `@theme static` with concrete values, and never `@import "tailwindcss" theme(static)`, which puts Tailwind's whole default palette in the editor. See the `pollora-theming` skill. + 3. **Add font assets** to the Laravel Vite plugin config: @boostsnippet('Vite Assets Config', 'js') diff --git a/src/Mcp/Prompts/UpgradePolloraV1332/UpgradePolloraV1332.php b/src/Mcp/Prompts/UpgradePolloraV1332/UpgradePolloraV1332.php new file mode 100644 index 0000000..5139847 --- /dev/null +++ b/src/Mcp/Prompts/UpgradePolloraV1332/UpgradePolloraV1332.php @@ -0,0 +1,58 @@ +installedFrameworkVersion(); + + return $version !== null && $this->appliesTo($version); + } + + /** + * Whether a framework version is one this upgrade starts from: 13.0 up to, + * not including, the first 13.32 pre-release. + */ + public function appliesTo(string $version): bool + { + $version = ltrim($version, 'vV'); + + return version_compare($version, '13.0.0', '>=') + && version_compare($version, '13.32.0-dev', '<'); + } + + public function handle(): Response + { + $content = $this->renderBladeFile(__DIR__.'/upgrade-pollora-v13-32.blade.php'); + + return Response::text($content); + } + + private function installedFrameworkVersion(): ?string + { + $version = InstalledVersions::getPrettyVersion('pollora/framework'); + + if ($version === null || str_starts_with($version, 'dev-')) { + $version = InstalledVersions::getVersion('pollora/framework'); + } + + return $version; + } +} diff --git a/src/Mcp/Prompts/UpgradePolloraV1332/upgrade-pollora-v13-32.blade.php b/src/Mcp/Prompts/UpgradePolloraV1332/upgrade-pollora-v13-32.blade.php new file mode 100644 index 0000000..cffa521 --- /dev/null +++ b/src/Mcp/Prompts/UpgradePolloraV1332/upgrade-pollora-v13-32.blade.php @@ -0,0 +1,217 @@ +@verbatim +# Pollora 13.x to 13.32 Upgrade Specialist + +You are upgrading a Pollora project from 13.x (13.0 to 13.4.x) to 13.32. The framework version now tracks the Laravel release it targets, hence the jump from 13.4 to 13.32. Work through the steps in order, measure each change (run the site, the tests, `curl` the pages), and do not report success on a step you have not checked. + +## Upgrade Process + +### 1. Assess Current State + +- Read `composer.json`: the `pollora/framework`, `laravel/framework` and `johnpbloch/wordpress` constraints, the `repositories` block, the `scripts`, and `extra.enable-patching` +- Run the test suite and note the baseline +- Search for the patterns listed in each section below and list every file affected before changing anything + +### 2. Create Safety Net + +- Work on a dedicated branch +- Commit `composer.lock` and `patches.lock.json` (if any) before updating + +### 3. Apply the Sections Below, in Order + +Dependencies first, then code, then the skeleton files, then themes. + +### 4. Verify + +- `composer install` exits 0 **and** WordPress is patched: `grep -c "function __wp" public/cms/wp-includes/l10n.php` answers 1 +- `php artisan discovery:clear && php artisan pollora:status` +- Load the home page, a post, a page, a 404 and wp-admin; with `WP_DEBUG` on, `curl -s | grep pollora:template` says which template answered +- Open the block editor: the theme's blocks are in the inserter + +--- + +# Upgrading from Pollora 13.x to 13.32 + +## Dependencies + +**Likelihood Of Impact: High** + +```json +"require": { + "johnpbloch/wordpress": "^7.1", + "laravel/framework": "^13.33", + "pollora/framework": "^13.32" +} +``` + +- `pollora/framework` 13.32 requires `illuminate/*` `^13.32`: upgrade Laravel first if needed +- While 13.32 is in beta, the constraint is `^13.32@beta` +- WordPress plugins and themes come from wp-packages instead of wpackagist. Replace the repository and rename the packages (`wpackagist-plugin/x` → `wp-plugin/x`, `wpackagist-theme/x` → `wp-theme/x`): + +```json +"repositories": [ + { "type": "composer", "url": "https://repo.wp-packages.org" } +] +``` + +## Patching WordPress (`cweagans/composer-patches` 2) + +**Likelihood Of Impact: High** + +The framework patches WordPress core so that its `__()` becomes `__wp()` and Laravel's helper keeps the name. composer-patches moves from 1 to 2, which behaves differently: + +- It ignores `extra.enable-patching`: remove it +- Once `patches.lock.json` exists it reads **only** the lock, and it patches a package only when that package is installed. A lock missing the WordPress patch leaves WordPress unpatched while `composer install` exits 0 — two functions named `__()`, a fatal error +- Add to `scripts.post-update-cmd`, before anything else: + +```json +"post-update-cmd": [ + "@composer patches-relock --no-interaction", + "@composer patches-repatch --no-interaction", + "@php artisan vendor:publish --tag=laravel-assets --ansi --force" +] +``` + +- Commit the generated `patches.lock.json` +- Check: `grep -c "function __wp" public/cms/wp-includes/l10n.php` must answer 1 + +## Artisan Commands Renamed + +**Likelihood Of Impact: Medium** + +Commands follow the Laravel colon convention: `pollora:env-setup` → `pollora:env:setup`, `pollora:make-theme` → `pollora:make:theme`, and likewise for `make-plugin`, `make-block`, `make-model`, `make-action`, `make-filter`, `make-posttype` (→ `make:post-type`), `make-taxonomy`, `make-wp-cli`, `delete-theme` (→ `theme:delete`). The old names still work as aliases. + +Search `composer.json` scripts, CI files, deploy scripts and docs for `pollora:[a-z]+-`. At least update `post-autoload-dump`: + +```json +"@php artisan pollora:env:setup --install" +``` + +## Modules Extracted to Packages + +**Likelihood Of Impact: Low (only code importing internal classes)** + +The Hook, Option and Ajax modules moved to `pollora/hook`, `pollora/option` and `pollora/ajax`, installed with the framework. The facades (`Action`, `Filter`, `Option`, `Ajax`) and the attributes do not change. Code importing the internal classes must follow them: + +| Before | After | +|---|---| +| `Pollora\Hook\Domain\Contracts\{Action,Filter,HookInterface,CallbackResolverInterface}` | `Pollora\Hook\Domain\Contract\…` | +| `Pollora\Hook\Domain\Services\AbstractHook` | `Pollora\Hook\Domain\Service\AbstractHook` | +| `Pollora\Hook\Infrastructure\Services\{Action,Filter}` | `Pollora\Hook\Adapter\Out\WordPress\{Action,Filter}` | +| `Pollora\Option\Application\Services\OptionService` | `Pollora\Option\Application\Service\OptionService` | +| `Pollora\Option\Domain\Contracts\OptionRepositoryInterface` | `Pollora\Option\Domain\Contract\OptionRepositoryInterface` | +| `Pollora\Option\Domain\Exceptions\*`, `Domain\Models\Option`, `Domain\Services\OptionValidationService` | `Domain\Exception\*`, `Domain\Model\Option`, `Domain\Service\OptionValidationService` | +| `Pollora\Option\Infrastructure\Repositories\WordPressOptionRepository` | `Pollora\Option\Adapter\Out\WordPress\WordPressOptionRepository` | +| `Pollora\Ajax\Domain\Models\AjaxAction` | `Pollora\Ajax\Domain\Model\AjaxAction` | +| `Pollora\Ajax\Domain\Contracts\AjaxActionRegistrarInterface` | `Pollora\Ajax\Port\Out\AjaxActionRegistrarPort` | +| `Pollora\Ajax\Infrastructure\Services\AjaxFactory` | `Pollora\Ajax\Factory\AjaxFactory` | + +Search: `Pollora\\(Hook|Option|Ajax)\\(Domain|Application|Infrastructure)`. + +## `Loop` and `Query` Facades + +**Likelihood Of Impact: Medium** + +Their classes were deleted long ago but the aliases stayed, so a page calling `Loop::` or `Query::` threw `Class not found`. The aliases are gone. Search `Loop::`, `Query::`. In Blade, use Sage Directives (`@posts`, `@title`, `@content`); in PHP, the WordPress function the method wrapped: + +| Before | After | +|---|---| +| `Loop::title($post)` | `get_the_title($post)` | +| `Loop::content()` | `apply_filters('the_content', get_the_content())` | +| `Loop::excerpt($post)` | `apply_filters('the_excerpt', get_the_excerpt($post))` | +| `Loop::thumbnail($size, $attr, $post)` | `get_the_post_thumbnail($post, $size, $attr)` (argument order changes) | +| `Loop::link($post)` | `get_permalink($post)` | +| `Loop::terms($taxonomy, $post)` | `get_the_terms($post, $taxonomy) ?: []` (argument order changes) | +| `Loop::postClass($class, $id)` | `'class="'.implode(' ', get_post_class($class, $id)).'"'` | +| `Loop::paginate($args)` | `paginate_links($args)` | + +`Query::` is replaced by the `PostQuery`, `MetaQuery` and `TaxQuery` facades, or `new WP_Query(...)`. + +## `Translater` Requires Its Domain + +**Likelihood Of Impact: Low** + +`new Translater($items)` now needs the text domain: `new Translater($items, 'my-theme')`. The old `'wordpress'` default relied on a key prefix that `pollora/helper-overrider` 1.2 removed, so it silently returned values untranslated. Search `Translater`. + +How `__()` routes, for reference: a string second argument is a WordPress text domain (`__('Text', 'my-theme')`), an array is a Laravel call with replacements, no argument tries Laravel then WordPress's `default` domain. + +## Blocks + +**Likelihood Of Impact: Medium (themes, plugins and modules with blocks)** + +- Blocks move from `resources/blocks/{slug}` to `resources/views/blocks/{slug}`. The old directory is still registered, with a deprecation notice in the log, until v15: `git mv resources/blocks resources/views/blocks` +- Pollora registers the blocks of every theme, plugin and module itself on `init`. Delete `app/Providers/BlocksServiceProvider.php`: a provider boots too late over HTTP and not at all for REST, so its blocks existed in WP-CLI only +- In `vite.config.js`, glob `./resources/views/blocks/*/{index,view}.{js,jsx,ts,tsx}` and `./resources/views/blocks/*/{editor,style}.css`, and limit full reloads to Blade files: `refresh: [...refreshPaths.filter((p) => p !== 'resources/views/**'), 'resources/views/**/*.blade.php']`. Running `pollora:make:block` once does it +- `pollora:make:block` now makes dynamic blocks rendered by `render.blade.php` by default; `--static` gives the former `save.jsx` block; `--dynamic` is deprecated +- Custom `BlockRegistrarInterface` implementations: `registerDirectory()` and `registerBlock()` gain a final `?string $basePath = null` parameter + +## Skeleton Files + +**Likelihood Of Impact: Medium** + +Compare with the skeleton (`Pollora/pollora`) and bring over: + +- **`routes/web.php`**: remove the `Route::wp('home'|'single'|'page'|'404', …)` entries the old skeleton declared. They took priority over the template hierarchy, so a theme's `single.blade.php` was ignored in favour of `view('post')`. Keep `Route::wp()` only for requests that need controller logic +- **Cache table**: `.env.example` uses `CACHE_STORE=database`; add Laravel's `cache` and `cache_locks` migration if missing (`php artisan make:cache-table`), or a fresh install answers 500 +- **`public/.htaccess`**: the trailing slash redirect now keeps the scheme behind a TLS proxy, redirects only GET and HEAD, and leaves `/wp-json` to WordPress: + +```apache +# Redirect Trailing Slashes If Not A Folder... +RewriteCond %{HTTPS} =on [OR] +RewriteCond %{HTTP:X-Forwarded-Proto} =https +RewriteRule ^ - [E=POLLORA_SCHEME:https] +RewriteCond %{ENV:POLLORA_SCHEME} !=https +RewriteRule ^ - [E=POLLORA_SCHEME:http] +RewriteCond %{REQUEST_METHOD} ^(GET|HEAD)$ +RewriteCond %{REQUEST_URI} !^/wp-json(/|$) +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_URI} (.+)/$ +RewriteRule ^ %{ENV:POLLORA_SCHEME}://%{HTTP_HOST}%1 [L,R=301] +``` + +- **`composer dev`** runs `@php artisan dev` + +## Themes: `theme.json` from `@theme` + +**Likelihood Of Impact: Medium (themes built with `wordpressThemeJson`)** + +Check the theme for three defects that combine into an editor offering some 290 Tailwind colours and semantic colours that resolve to nothing: + +1. `@import "tailwindcss" theme(static);` in `app.css` → `@import "tailwindcss";` +2. A `copy-theme-json` step in `vite.config.js` copying the built `theme.json` over the theme's own → remove it, and remove the generated `palette`, `fontSizes`, `fontFamilies` and `radiusSizes` from the theme's `theme.json` (the base wins over `@theme` on any slug it defines) +3. `@theme` values written `var(--wp--preset--color--x, #hex)` → concrete values in `@theme static`, plus a rule pointing the utilities at the presets: + +```css +@theme static { + --color-primary: #1f2937; + --text-base: 1rem; + --radius-lg: 0.5rem; +} + +@layer base { + :root { + --color-primary: var(--wp--preset--color--primary, #1f2937); + } +} +``` + +Check: the built `public/build/theme/{slug}/assets/theme.json` lists only the theme's colours, each with a value. See the `pollora-theming` skill for keeping a value out of Tailwind. + +## New in 13.32 (optional) + +- `pollora_register(ModuleType::Theme)` in `functions.php`; `pollora_register(ModuleType::Plugin, 'slug', __DIR__)` in a plugin +- `#[Ajax('action', access: AjaxAccess::ALL)]` for admin-ajax handlers (logged-in only by default) +- `#[Ability(...)]` and the `Ability` facade for the WordPress Abilities API (WordPress 6.9+) — see the `pollora-abilities` skill +- `#[SkipDiscovery]` to keep a class out of discovery; `config/discovery.php` (publishable) to skip classes (`skip_classes`) or paths (`skip_paths`) +- `config/login.php` in a theme styles the login screen from `theme.json` +- `get_theme_file_uri()` returns the URL the build gave a theme file +- With `WP_DEBUG` on, each page rendered through the hierarchy carries `` + +## Post-Upgrade Cleanup + +```bash +php artisan discovery:clear +php artisan optimize:clear +php artisan migrate +cd themes/your-theme && npm install && npm run build +``` +@endverbatim diff --git a/tests/Feature/Prompts/UpgradePolloraV1332Test.php b/tests/Feature/Prompts/UpgradePolloraV1332Test.php new file mode 100644 index 0000000..b6d7f33 --- /dev/null +++ b/tests/Feature/Prompts/UpgradePolloraV1332Test.php @@ -0,0 +1,25 @@ +appliesTo($version))->toBeTrue(); +})->with(['13.0.0', 'v13.2.0', '13.4.3']); + +it('does not apply to 12.x, nor to 13.32 and later, pre-releases included', function (string $version) { + expect((new UpgradePolloraV1332)->appliesTo($version))->toBeFalse(); +})->with(['12.5.0', '13.32.0-beta', '13.32.0-beta.8', '13.32.0', '13.33.0', '14.0.0']); + +it('renders the upgrade guide', function () { + $guide = (string) (new UpgradePolloraV1332)->handle()->content(); + + expect($guide) + ->toContain('patches-relock') + ->toContain('resources/views/blocks') + ->toContain('Pollora\\Hook\\Adapter\\Out\\WordPress') + ->toContain('@import "tailwindcss";') + ->toContain('`@posts`') + ->not->toContain('@verbatim'); +}); From d4264c4458a5be2c860ead3b3d71384bccb0ff77 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Mon, 28 Sep 2026 10:06:22 +0200 Subject: [PATCH 4/4] chore: test against Laravel 13 and framework 13 orchestra/testbench stopped at ^10, Laravel 12, so the suite ran against pollora/framework 12.0 whatever the package claimed. It accepts ^11 now, and pollora/framework is bounded to ^12.0 || ^13.0 instead of *. On framework 13, Pollora's Route class has the methods WordPressRoutes probed with method_exists(): the probes and their phpstan-ignore comments go. --- CHANGELOG.md | 21 +++++++++++++++++++++ composer.json | 4 ++-- src/Mcp/Tools/WordPressRoutes.php | 13 +++++-------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d04612..59d0122 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **Pollora 13.x → 13.32 upgrade prompt** (`upgrade-pollora-v13-32`), registered when Pollora 13.0 to 13.4.x is installed: dependencies and wp-packages, composer-patches 2 (`patches-relock`, `patches.lock.json`, checking that WordPress is patched), renamed Artisan commands, classes moved to `pollora/hook`, `pollora/option` and `pollora/ajax`, `Loop`/`Query` removal, `Translater` domain, blocks in `resources/views/blocks` and registered without a provider, skeleton files (`routes/web.php`, cache table, `.htaccess`), and the theme.json defects fixed in themes (`theme(static)`, the copy step, cyclic `@theme` values) +- **`pollora-abilities` skill**: the WordPress Abilities API with `#[Ability]` and the `Ability` facade +- `active_theme_info` reports `blocks_in_deprecated_directory`, the blocks still in `resources/blocks` + +### Changed + +- **Guidelines**: the facades that exist (no `Loop` or `Query`, which were removed; `Ability`, `PostQuery`, `MetaQuery`, `TaxQuery`, `Mail`, `Constant` added), `__()` routing, blocks, `pollora_register()`, the design tokens in `@theme`, the template marker, `#[Ajax]`, `#[Ability]`, `#[SkipDiscovery]`, and the full list of Artisan commands +- **13.x guidelines**: `illuminate/*` `^13.32`, WordPress 7, the extracted packages +- **`pollora-blocks` skill** rewritten for `resources/views/blocks`, dynamic Blade blocks by default (`render.blade.php`, `--static`), registration by Pollora without a service provider, and the migration from `resources/blocks` +- **`pollora-theming` skill**: `pollora_register()`, the login screen (`config/login.php`), `get_theme_file_uri()`, the template marker; `tailwind.config.js` dropped +- **`pollora-rest-api` skill**: AJAX handlers with `#[Ajax]` +- **12 → 13 upgrade prompt**: no longer adds a `copy-theme-json` build step, which froze a theme's palette; blocks section updated +- `pollora/framework` is constrained to `^12.0 || ^13.0` instead of `*`; `orchestra/testbench` accepts `^11.0`, so the tests run against Laravel 13 and framework 13 instead of Laravel 12 and framework 12.0 + +### Fixed + +- `active_theme_info` listed no block for a theme made since v13.32: it read `resources/blocks` only. It reads `resources/views/blocks`, and counts a directory as a block only when it holds a `block.json` +- `pollora-post-types` skill: `pollora:make:post-type` (was `pollora:make-post-type`) + ## [0.2.0] - 2026-06-23 ### Added diff --git a/composer.json b/composer.json index 5c18fd8..4b2051e 100644 --- a/composer.json +++ b/composer.json @@ -13,12 +13,12 @@ "require": { "php": "^8.3", "laravel/boost": "^2.0", - "pollora/framework": "*" + "pollora/framework": "^12.0 || ^13.0" }, "require-dev": { "driftingly/rector-laravel": "^2.3", "laravel/pint": "^1.0", - "orchestra/testbench": "^9.0|^10.0", + "orchestra/testbench": "^9.0|^10.0|^11.0", "pestphp/pest": "^3.0", "phpstan/phpstan": "^2.0" }, diff --git a/src/Mcp/Tools/WordPressRoutes.php b/src/Mcp/Tools/WordPressRoutes.php index 4740649..ccb3340 100644 --- a/src/Mcp/Tools/WordPressRoutes.php +++ b/src/Mcp/Tools/WordPressRoutes.php @@ -10,6 +10,7 @@ use Laravel\Mcp\Response; use Laravel\Mcp\Server\Tool; use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly; +use Pollora\Route\Infrastructure\Models\Route as PolloraRoute; #[IsReadOnly] class WordPressRoutes extends Tool @@ -22,14 +23,10 @@ public function handle(Request $request): Response { $routes = Route::getRoutes(); $result = []; - $polloraRouteClass = 'Pollora\Route\Infrastructure\Models\Route'; - /** @var LaravelRoute $route */ foreach ($routes->getRoutes() as $route) { $action = $route->getAction(); - $isWordPress = $route instanceof $polloraRouteClass - && method_exists($route, 'isWordPressRoute') - && $route->isWordPressRoute(); // @phpstan-ignore-line + $isWordPress = $route instanceof PolloraRoute && $route->isWordPressRoute(); $routeInfo = [ 'uri' => $route->uri(), @@ -43,9 +40,9 @@ public function handle(Request $request): Response $routeInfo['controller'] = $action['controller']; } - if ($isWordPress && method_exists($route, 'hasCondition') && $route->hasCondition()) { // @phpstan-ignore-line - $routeInfo['wp_condition'] = $route->getCondition(); // @phpstan-ignore-line - $routeInfo['wp_condition_params'] = $route->getConditionParameters(); // @phpstan-ignore-line + if ($route instanceof PolloraRoute && $isWordPress && $route->hasCondition()) { + $routeInfo['wp_condition'] = $route->getCondition(); + $routeInfo['wp_condition_params'] = $route->getConditionParameters(); } $result[] = $routeInfo;