Skip to content

Changelog

Release notes for Pollora, the Laravel framework for WordPress: every stable and pre-release version of pollora/framework, newest first.

Pollora's version numbers follow the Laravel release it is built on. The skeleton (pollora/pollora) is tagged with the same number as the framework it ships. Releases are also published on GitHub.

v13.34.1

· View on GitHub

Removed

  • The use_default_wp_theme_directory key of config/wordpress.php. Nothing ever read it: setting it to true changed nothing, and themes always live in themes/ (#297)

Fixed

  • Under WordPress 7, a classic theme’s block styles and global-styles were printed at the bottom of every page, after the content they style — a flash of unstyled content on slow connections — and the head kept two empty placeholders. WordPress loads those styles on demand at wp_footer, then moves them back into the head through its template enhancement output buffer, which only starts when template-loader.php includes a template: a Blade page includes none. A WordPressTemplateEnhancement middleware now plays the buffer’s part on the response — wp_template_enhancement_output_buffer_started before the view renders, the wp_template_enhancement_output_buffer filter and wp_finalized_template_enhancement_output_buffer on the HTML — for Route::wp() routes and the template hierarchy, so any plugin built on that filter sees Pollora’s pages too. A plain Laravel route that prints wp_head()/wp_footer() can add the middleware itself
  • pollora:install without a terminal — --no-interaction, CI, or pollora new driving it — stopped on “Site title is required” unless every option was passed: the prompts it could not show were still required. It now fills what is missing with a working local site — the project name as title (the first label of the APP_URL host: under DDEV the directory is always html), admin at admin@<APP_URL host>, a generated password shown once (also with --install), en_US, not indexed — and keeps every option it is given
  • pollora:status and the dashboard reported a post type or taxonomy under a slug derived from its class name, ignoring the attribute: #[PostType('synthese-presse')] class SyntheseDePresse was listed as synthese-de-presse, a post type that does not exist, and its plural label was ignored too. Both read the attribute now, through PostType::resolveSlug() / Taxonomy::resolveSlug(), the rule discovery registers with, so they cannot disagree again (#298)

v13.34.0

· View on GitHub

The first stable release since v13.4.4, on Laravel 13.34. It gathers every change from v13.32.0-beta to v13.34.0-beta.2 below; coming from 13.4, read those entries, or run Nectar’s upgrade-pollora-v13-32 prompt (full comparison).

Fixed

  • A plugin with neither app/ nor src/ — one that only ships blocks, which need no class — was discovered from its root, node_modules included: 2 to 7 seconds on every request in debug mode, measured on a blocks-only plugin with a Vite build. Its other top-level directories are scanned instead, never node_modules, vendor, bower_components, build, dist, public, resources, assets, languages, lang or a hidden directory

v13.34.0-beta.2 Pre-release

· View on GitHub

Added

  • pollora:doctor: checks a project for the failures that stay silent — the site renders, the command exits 0 — and prints, under each, the command that fixes it. Each check comes from a failure met in practice: WordPress core not patched or __() not Pollora’s; patches.lock.json missing or older than the framework’s patches; .env names Pollora does not read (DB_NAME, WP_HOME…) or MySQL settings on a sqlite connection; configuration or routes cached outside production; classes missing from the discovery cache; for the theme, every Pollora plugin and every enabled module: a build missing, written to another folder than Pollora reads, or a hot file pointing at a dev server that is stopped or not exposed, a directory symlinked under another name, %theme_*%/%plugin_*% placeholders or .stub files left from a copied template, blocks still in the legacy resources/blocks; pattern files WordPress never registers or has not cached; Route::wp() routes answering in place of a block theme’s templates. --json for scripts; exits 1 on an error
  • The same checks in WordPress’s Site Health (Tools › Site Health), with a “Pollora” badge, plus one that only a web request can make: every block of the theme, the Pollora plugins and the modules is registered. That is the boot a visitor gets — blocks were once registered under WP-CLI but not over HTTP, which a console check would have passed

Fixed

  • Deleting a navigation menu (wp menu delete, or the Menus screen) raised a TypeError once the menu was already gone: delete_nav_menu is the delete_{$taxonomy} hook, which passes the term ID first, and the listener expected a WP_Term. MenuDeleted now receives the menu WordPress copied before deleting it
  • While Vite ran hot, its client was enqueued with WordPress’s version appended (@vite/client?ver=7.1.2). The modules Vite serves import /@vite/client by its bare URL, so the browser loaded the client twice, as two modules with two HMR connections. It is enqueued with no version, like the entries (regression from v13.34.0-beta)
  • On a branch install (dev-develop, 13.x-dev), Site Health and the admin notice announced “Pollora 13.4.4 is available”: version_compare() ranks a branch name below every release. A development build is no longer compared with releases — Site Health says it is one, the notice stays silent — and the dashboard, the admin menu badge and pollora:status share that one rule, which they each duplicated and missed 13.x-dev. Site Health’s info tab now labels the compared version “Latest stable version”, since pre-releases are not counted

Changed

  • pollora:make:theme activates the generated theme only where the site needs one. A site with no usable theme — a first install — gets it without a question; a site that already has one keeps it unless the answer is “yes”, now the default “no”, so --no-interaction never replaces a working theme. --activate and --no-activate settle it without asking, and pollora:install passes --activate. Activation goes through switch_theme(), which fires switch_theme and after_switch_theme; the options were written directly before, so those hooks never ran

v13.34.0-beta Pre-release

· View on GitHub

The framework’s version tracks Laravel’s: this beta requires Laravel 13.34.

Added

  • A third pollora:make:theme template, Magazine (magazine → pollora/theme-buzz): a Full Site Editing block theme whose templates, parts and patterns are edited in the Site Editor, next to default and ecommerce. The missing-theme page and admin notice list it too

Fixed

  • A Vite script was printed before WordPress’s import map, which Firefox and Safari then ignore: any WordPress script module on the page — the navigation block’s, the search block’s, the image lightbox’s — failed on @wordpress/interactivity was a bare specifier, so the block did nothing. Chromium tolerates the order, which hid it. In a classic theme the import map is always in the footer, so any theme with a Vite script in the head was affected as soon as an author inserted such a block. The Vite client of the dev server had the same problem
  • A block theme’s own 404.html answered with HTTP 200. WordPress core resolves it to wp-includes/template-canvas.php, which is never a Blade view, so it always rendered through FrontendController’s raw-PHP-template branch — the only branch that never looked at is_404(). Measured on a fresh block theme: right content, wrong status
  • A real 404 lost its error404 body class, and every page served by the template hierarchy carried a meaningless one built from its path (any-no-such-page). The WordPressBodyClass middleware was meant for Laravel routes, which WordPress’s own resolution calls a 404, but it only ran on WordPress routes — the {any} fallback included — where WordPress’s verdict is the right one. So it did the opposite of its job on both sides: a Laravel route (Route::get('/dashboard/{tab}')) kept error404, is_404() true and a “Page not found” title over its 200 response

Changed

  • Requires Laravel 13.34: illuminate/* ^13.34 (was ^13.32). Measured on laravel/framework v13.34.0: the full suite, Pint, PHPStan and Rector pass unchanged
  • The WordPressBodyClass middleware is replaced by a RouteMatched listener, ApplyApplicationRouteContext, which runs on every route: a route WordPress answers (Route::wp() and the template-hierarchy fallback, both flagged isWordPressRoute()) keeps WordPress’s classes and verdict untouched; any other route has is_404() cleared and its URI segments added as body classes (dashboard tab-settings). A middleware could not do this — Laravel routes are not given the WordPress middleware stack
  • On the front end and in the admin, a Vite script is enqueued as a WordPress script module (wp_enqueue_script_module), so WordPress places it after its import map, as it does its own modules: in the head of a block theme (the footer with loadInFooter()), always in the footer of a classic theme. What a module cannot take — dependencies(), localize(), inline() — goes on a classic companion script, {handle}-data, which runs before the module. The editor, login screen and Customizer are unchanged

v13.32.0-beta.9 Pre-release

· View on GitHub

Added

  • <InnerBlocks /> in a block’s render.blade.php: the block’s inner blocks are edited in place in the editor, inside the rendered template, and printed in place of the tag on the page, in a div carrying the tag’s class (pollora-inner-blocks by default). The tag takes Gutenberg’s inner blocks options — allowedBlocks, template, templateLock, orientation… — as attributes, JSON for arrays and objects. Modelled on ACF’s <InnerBlocks />. A render.php template gets it too
  • The block editor runtime window.pollora.blocks (script handle pollora-block-editor, a dependency of the editor script of every block with a render template): bladeEdit(metadata) previews the template through the core block-renderer route and makes its <InnerBlocks /> editable; save stores the inner blocks. Shipped inline, as nothing under vendor/ has a public URL
  • $isPreview in a block template: true while it renders for the editor’s preview
  • develop is aliased to 13.x-dev (extra.branch-alias). Without it, dev-develop satisfied no version constraint: a project on dev-develop could not install a package requiring pollora/framework ^13.0 — pollora/nectar 1.1 does — without an inline alias of its own

Fixed

  • pollora:make:block --inner-blocks made a dynamic block whose inner blocks were neither editable in its preview, saved in the post, nor printed by its template. It now writes <InnerBlocks /> in render.blade.php
  • TemplateMarker’s documentation no longer says every response goes through template_include: responses from Route::wp() and Laravel routes bypass it and carry no marker, as the hierarchy browser tests assert
  • “Validate Changelog” no longer fails the pull request of every release. It required a non-empty [Unreleased] on any pull request to main, which a release from develop or release/* has emptied into the version’s section by construction; it now checks hotfixes only

Changed

  • pollora:make:block generates dynamic blocks on the runtime: edit.jsx is window.pollora.blocks.bladeEdit(metadata) instead of ServerSideRender, save is window.pollora.blocks.save instead of () => null, and @wordpress/server-side-render is no longer added to package.json. Blocks made before keep working
  • The framework no longer declares the wpackagist repository, from which it required nothing. Composer still fetched its metadata on every install, so a wpackagist network error failed the build — measured: the nightly of 2026-09-27 failed “Code Quality” on curl error 56 from wpackagist.org
  • The installed package no longer carries the test suite, the CI workflows and the tooling configs: .gitattributes leaves them out of the archive Composer installs. The browser tests stay reachable by cloning the repository, which is how theme CI fetches them

Removed

  • get, a grep output committed by mistake

v13.32.0-beta.8 Pre-release

· View on GitHub

No change to the framework’s code: this beta ships the browser test suite that now guards it, 73 tests per browser.

Added

  • Browser tests of the template hierarchy. Two fixture themes: one has a template for every case, the other index.blade.php alone. Each URL is read in the framework’s template marker, in the template’s own output and in the HTTP status: front-page, home, page-{slug}, page-{id}, a custom page template, page, single, single-{type}, archive-{type}, archive, taxonomy-{tax}-{term}, taxonomy-{tax}, category-{slug}, tag, author-{nicename}, date, search, and 404 with a 404 status; a Blade view over a PHP template of the same name; Route::wp() and Laravel routes over the hierarchy. With index alone every case falls back to it, and an unknown URL still answers 404. Replayed: without the 404 view, or with the marker disabled, the suite fails
  • Browser tests of the framework’s features, through a fixture plugin that declares each by attribute and checks it by its effect: #[Filter] and #[Action], a #[PostType] (REST type, archive, admin menu), #[WpRestRoute] (a public route; an IsAdmin one that refuses a visitor and answers an administrator), #[Ajax] for visitors and logged-in users, a script enqueued through the Asset facade, every script and stylesheet of the home page loading with nothing over plain http://, get_theme_file_uri() giving the active theme’s built entry a URL that answers, no server path in the page a visitor gets, the theme’s menu locations and login screen, and __() sending a text domain to WordPress’s catalogues and replacements to Laravel. Replayed: with the v13.32.0-beta.6 theme URI fix undone, or with WordPress’s side of __() disabled, the suite fails
  • The browser tests run in Firefox and WebKit as well as Chromium every night, and on a manual run of the workflow (E2E_BROWSERS). Pull requests keep Chromium alone. The schedule runs from main, so it starts with this release

v13.32.0-beta.7 Pre-release

· View on GitHub

Fixed

  • pollora:make:block refuses a theme or plugin that cannot build a block, and says why. A plugin made without assets has neither package.json nor vite.config.js: the block was written anyway, could not be built, and the npm install the command advised walked up to the site’s own package.json and installed there. It now stops before writing anything and names the missing files
  • pollora:make:plugin --asset includes the asset files. The option takes a value, so the bare flag read as null, which the defaults applied without a terminal turned into false: --asset alone left the assets out. --asset=false still leaves them out, and the other options keep their defaults
  • A block title holding a quote (--title="Owner's Hero") no longer breaks the JavaScript pollora:make:block writes. render.blade.php escaped it; edit.jsx and save.jsx printed it raw inside a single-quoted string, so the block failed to build
  • A block’s editor script depends on every WordPress script its code imports. BlockRegistrar gave each editor script a fixed list — wp-blocks, wp-element, wp-block-editor, wp-i18n — while @roots/vite-plugin turns every @wordpress/* import into a global and records the matching handles in editor.deps.json. A block importing @wordpress/components or @wordpress/server-side-render worked only when something else happened to load that script. The handles in the build’s editor.deps.json are now added to the defaults; in dev mode, which has no build, the defaults stand
  • Blocks shipped by themes, plugins and modules exist in the block editor, on the page and in the REST API. They were registered by a BlocksServiceProvider of the module’s own, and over HTTP no shape of that provider works: WordPress is loaded, and init has fired, before theme and plugin providers boot, so the provider pollora:make:block wrote — hooking init from boot() — was never called, and a REST request is answered before those providers boot at all. Blocks existed in WP-CLI only, which is where they had been checked. Measured on a fresh install: theme-default’s hero and call-to-action were missing from the editor and from /wp/v2/block-types. The framework now registers every module’s resources/views/blocks (and the former resources/blocks) itself on init, from a hook set while it boots — before WordPress loads — and creates the asset container of a Laravel module that ships blocks. pollora:make:block no longer writes a provider. A BlocksServiceProvider kept from an earlier release is harmless: a block WordPress already holds is skipped. Covered by browser tests that insert each block in the editor, reload it, and find it on the published page
  • Themes and plugins keep working on a site whose autoloader was dumped with --classmap-authoritative. Their namespaces (Theme\…, Plugin\…) were added at runtime to Composer’s root loader, and an authoritative loader answers from its classmap alone: it never looked at them, so every front-end page answered 404 (Class "Theme\Apiary\Walkers\MenuPrimary" not found) while wp-login.php still worked. Module namespaces now live in a class loader of their own, registered after Composer’s, and shared by the theme and plugin autoloaders under the ModuleAutoloader::CLASS_LOADER container key. It has no vendor directory, so it stays out of ClassLoader::getRegisteredLoaders() and cannot move Application::inferBasePath(). Measured on a live site in both modes: pages, the PHPUnit and HTTP suites, and the theme sweep all pass. The root loader is no longer bound to Composer\Autoload\ClassLoader in the container as a side effect
  • php artisan test, and anything else that asks Laravel for its base path, no longer resolves it to a plugin’s directory. ModuleAutoloader::register() registered the root Composer loader again although it was already active, and Composer answers that by moving it to the end of ClassLoader::getRegisteredLoaders() — behind the loader of any plugin shipping its own vendor/, such as Query Monitor. Application::inferBasePath() reads the first entry of that list, so on such a site 38 of 40 of its PHPUnit tests failed opening plugins/query-monitor/bootstrap/app.php. Composer’s InstalledVersions::getRootPackage() read the same list and reported the plugin as the root package, over HTTP, in artisan and in WP-CLI. Namespaces added with addPsr4() take effect on an active loader, so the loader is now only registered when it is not already. The bug dates back to June 2025 and shipped in v13.4.4
  • patches/mockery-php84-nullable.patch is back on main, byte for byte as v13.4.4 shipped it. The v13.4.x line declares that patch by the URL refs/heads/main/patches/mockery-php84-nullable.patch, and a released composer.json cannot change; removing the file from main on 2026-09-17 made the URL answer 404, and composer-patches v1 skips a patch it cannot fetch with only a warning — so every v13.4.x install since went without it. Nothing on the 13.32 line references the file: its patch URLs are pinned to commits
  • The patches this package asks its consumers to apply are pinned to the commit that produced them, instead of being served from refs/heads/main. A released composer.json cannot be changed, so a branch URL moves under every version already published: removing mockery-php84-nullable.patch from main on 2026-09-17 turned it into a 404 for every v13.4.x install, which composer-patches v1 skips with only a warning. With composer-patches v2 a moved patch is worse — its checksum no longer matches patches.lock.json and the install fails. A Patches CI job now proves each URL pins a commit in the branch’s history whose file matches the working tree. Versions already released keep the URLs they shipped with
  • Generators write a theme’s, plugin’s or module’s classes where its autoloader reads them. The autoloaders map a namespace onto app/ when it exists and onto src/ otherwise — one directory, never both — while every --plugin, --theme and --module generator wrote into app/ unconditionally. On a target keeping its classes in src/, that was worse than one misplaced file: creating app/ flipped the autoloader and discovery over to it, and every class already in src/ stopped being loaded. They now resolve the directory with the autoloaders’ own precedence, and a target with neither still gets app/
  • The BlocksServiceProvider the scaffolder writes registers its blocks on init, which is when WordPress accepts block registrations at all. It called registerDirectory() straight from boot(), and providers boot before WordPress has defined register_block_type() — BlockRegistrar answers that by returning immediately, with no error and no notice, so the blocks simply never existed. theme-default hit this, shipped v1.4.0 with it, and fixed its own copy by hand; the stub that writes every other provider kept the broken shape, so each new theme and plugin was born with the bug
  • pollora:make:block writes the BlocksServiceProvider when the target has none. A blocks directory that is not empty was taken as proof that the infrastructure was in place, and it usually is — but blocks registered by something else live there too, and none of them needs the provider that registers Vite-built blocks. In a target like theme-apiary, whose only shipped block is an ACF one, every block ever scaffolded was written to disk, built by Vite, and registered by nobody: nothing failed, nothing reached the log, and the block simply never appeared in the editor — permanently, since the directory only gets less empty. Measured on a live site, then pinned end to end in the theme’s CI. Only the provider is created; the npm dependencies and the initial Vite patch still belong to a genuinely first block
  • composer install and composer update no longer fail on a Pollora site that has no terminal. pollora:env:setup runs from composer’s post-autoload-dump hook, so it fires inside container builds, deployments and CI; when the database was not reachable it reached for Laravel Prompts, which throws where there is nothing to prompt, and composer exited 1 reporting a prompting problem rather than a database one. Nothing in that command is a gate — pollora:install is what refuses to continue without a database — so with no terminal it now names what is wrong, names the command to run once the database is up, and lets composer finish

Changed

  • The block editor shows what the page shows for a block made by pollora:make:block. A dynamic block’s edit.jsx renders render.blade.php through ServerSideRender for the block’s current attributes, and a static block’s mirrors the markup save.jsx writes; both used to show a placeholder (”… – Block Editor”) the page never displayed. A block with --inner-blocks keeps the InnerBlocks editor, which a server render cannot provide. @wordpress/server-side-render joins the npm dependencies added with a first block. Checked in a browser: the preview holds the rendered text, and the old stub fails the same test
  • cweagans/composer-patches moves from ^1.7 to ^2.0. It is the plugin that applies the WordPress l10n patch — renaming WordPress’s __() to __wp() so Laravel’s helper can have the name — on every Pollora site. Measured before merging: on a fresh install, v2 resolves the framework’s patch as a dependency patch and applies it, and the skeleton’s check that WordPress core carries it passes. Two things change for a project. v2 writes a patches.lock.json recording each patch’s sha256, and on a machine without the patch cached it downloads the patch again and fails with HashMismatchException when the file no longer matches — so a patch served from a moving URL can break fresh installs of a project that locked an earlier version. And the skeleton’s enable-patching option is no longer read: v1.7.3 checked it and defaulted to off, v2 resolves dependency patches unconditionally
  • The commit-hook tooling is declared where it belongs. @commitlint/cli, @commitlint/config-conventional, husky, lint-staged and prettier sat under dependencies rather than devDependencies, so every advisory in their tree was reported against this repository at runtime scope — ten high-severity alerts describing packages no Pollora site has ever loaded. Nothing installs differently: npm ci installs devDependencies by default, and the commit hook was checked after the move
  • Every open npm advisory clears: fast-uri moves to 3.1.8 and js-yaml to 4.3.2, past the 3.1.6 and 4.3.2 that patch them. npm audit goes from 2 high-severity vulnerabilities to 0
  • The contribution guide documents the pull request title convention. CI validates the title of every pull request against the conventional commit format, and rejected one said only that the check had failed — a rule enforced on a first contribution and published nowhere. The allowed types are listed, along with the fact that Validate Changelog only runs on release pull requests into main and never concerns a contribution to develop

v13.32.0-beta.6 Pre-release

· View on GitHub

Fixed

  • get_theme_file_uri() answers the URL the build gave a theme file, instead of an empty string. It returned '' for every path there is — measured on a live site, on the front end as much as on wp-login.php. WordPress hands the theme_file_uri filter both the URL it built and the file it was asked for; the file was thrown away and rebuilt by subtracting get_stylesheet_directory_uri() from the URL, then handed to the asset resolver, which prefixes the container’s own root a second time. resources/assets/app.js was looked up as resources/assets/resources/assets/app.js, never found, and the failure was logged and swallowed — one line in laravel.log per call. Both spellings now resolve: the path from the theme root a caller would write, and the path relative to the container root the manifest is keyed on. A theme’s own directory is not web-served on a Pollora project — measured, /themes/… and /content/themes/… both 404 while /build/theme/… serves — so the build is the only address a theme file has. Anything the build does not know about is handed back untouched rather than emptied
  • The server’s filesystem path no longer reaches the public HTML. get_theme_root_uri() can only turn a theme root into a URL when it sits under WP_CONTENT_DIR; a Pollora project registers its themes at the project root, and for a root it cannot map WordPress returns the path verbatim — so get_stylesheet_directory_uri() answered /var/www/html/themes/apiary, and WordPress’s speculative-loading rules printed it into the <head> of every front-end page. Measured: one occurrence per page before, zero after. Only an answer that is not a URL is replaced, so a project keeping its themes under content is untouched

v13.32.0-beta.5 Pre-release

· View on GitHub

Added

  • The login screen wears the theme’s design, from the theme’s own theme.json. WordPress loads the theme on wp-login.php but emits none of its design there — measured: zero occurrences of wp--preset--color in the HTML of a login screen — so every site logs in through the same grey form whatever it looks like elsewhere. A theme that ships a config/login.php now gets its colours, radii and typography printed on login_head, its own logo above the form, and its home page behind that logo instead of wordpress.org. Nothing is duplicated: the design stays in theme.json, and restyling a theme restyles its login screen. Sign-in, lost password, reset, register and the confirm-admin-email prompt are all covered, being one screen as far as login_head is concerned. Strictly opt-in — a theme with no config/login.php gets WordPress’s screen, byte for byte, so upgrading the framework never changes the page people sign in through. See Theming → Login screen
  • pollora/login/palette, pollora/login/logo, pollora/login/styles and pollora/login/credit filters, for a module or a plugin to take over any part of the login screen without touching the theme
  • Discovery says so when one location takes more than 250ms to scan, naming the location, the time and how many structures it found. The cost above was absorbed in complete silence for as long as it existed — no log line, no notice, nothing that measured it. Debug mode only: with the cache on, the cost is paid once

Changed

  • A theme’s config/login.php is loaded on init, like menus.php, sidebars.php and templates.php, so __() works in it. WordPress fires init through wp-load.php before wp-login.php fires login_init, so the config is in place well before anything reads it

Fixed

  • Attribute discovery no longer walks a plugin’s node_modules. Themes and modules have always been scanned at app/, falling back to src/ — the two directories the autoloader maps Plugin\{Name}\ onto — while plugins were scanned at their root, which is where node_modules/ lands as soon as a plugin has a Vite build. Symfony’s Finder enumerates every file before filtering by extension, so the walk costs everything and the extension check costs nothing: measured on a demo plugin with a block build, 69,741 files walked on every request to reach the three PHP files the plugin owns — 1,691 ms against 1 ms for the same directory without node_modules. It also discovered five WordPress core classes shipped inside @wordpress/style-engine and handed them to the discovery pipeline, which is its own kind of wrong. With APP_DEBUG on, discovery keeps no cache, so a site paid this on every request: the home page of a site with one such plugin went from 3.0s to 0.78s, and from 691MB of peak memory to 37MB under Blackfire. A plugin shipping neither app/ nor src/ is still scanned at its root, so one keeping its classes at the top level is unaffected

v13.32.0-beta.4 Pre-release

· View on GitHub

Added

  • Every page says which template answered it, as an HTML comment on wp_head, whenever WP_DEBUG is on. WordPress offers no way to see which template rendered a request short of reading the code and guessing, and a Blade hierarchy on top makes the guess harder — several files could plausibly have answered, and which one did is the whole question when a page comes back wrong. Themes had started marking their own views by hand, one at a time, and drifting: theme-apiary marks most of its templates and not its front page. This is derived from the template the request actually resolved to, so it cannot be forgotten, and it reaches every theme without one of its own. A comment changes no markup and no styling; nothing at all is emitted in production, and the path is relative to the project so it never says where the site lives on disk

Fixed

  • A failed starter download says what happened instead of raising a TypeError. make:theme and make:plugin fetch their template from GitHub and, when that fails, fall back to copying one bundled with the framework — except none is bundled: neither src/Theme/stubs/ nor src/Plugin/stubs/ exists. getTemplatePath() declared string and returned realpath()’s false, so a transient network failure surfaced as getTemplatePath(): Return value must be of type string, false returned, naming a method the user has never heard of and saying nothing about the download. Measured on CI the first time GitHub did not serve an archive. Both commands now fail with a sentence that names the cause
  • A REST controller declaring WP_REST_Request $request receives it. Handler arguments were built from the route parameters alone, so that type hint had get_param('request') looked up — no route declares a parameter by that name — and the method was invoked with null, fatalling on a TypeError before a line of it ran. Every endpoint whose handler asked for the request answered 500. A parameter type-hinted WP_REST_Request now gets the request; everything else still comes from the route, and builtin types are left alone since they name route values
  • WordPress’s own entry points answer their real status again. wp-login.php, wp-links-opml.php and the rest are run directly by PHP, with Pollora loaded along the way through wp-config.php; it resolved the URL as a content request anyway, and /cms/wp-login.php matches the attachment rewrite rule [^/]+/([^/]+)/?$. No such attachment exists, so WordPress sent a 404 header — and then rendered a perfectly good login form underneath it. The query now runs only when Laravel’s front controller is the script being executed, which covers any entry point WordPress or a project adds later without naming it. pollora_loaded still fires either way
  • /cms/ no longer answers with the source of a Blade template. The WordPress installation root is served by WordPress’s own index.php, whose template-loader.php includes whatever the template hierarchy hands it — and Pollora puts Blade sources in that hierarchy, since it renders them through Laravel. PHP printed one verbatim, directives and all. A front-end request that reaches that loader has bypassed Laravel and was looking for the site, so it is redirected to the home URL
  • make:theme and make:plugin download the highest version of a starter, not whichever tag GitHub listed first. That order follows what was pushed most recently, so a fix tagged on an older line — 1.2.1 published after 1.4.0 — would have been handed to everyone scaffolding, a downgrade with nothing to notice it by. Tags are now compared as versions, pre-releases lose to any stable version so tagging a beta does not push it onto people who asked for nothing in particular, and a tag that is not a version at all is left out of the comparison rather than winning it. A repository whose tags follow some other scheme keeps its previous behaviour. The request also asks for 100 tags instead of the default 30, which could leave the highest version on an unread second page

v13.32.0-beta.3 Pre-release

· View on GitHub

Fixed

  • The block editor now shows the theme’s own styles, so a block looks in the back office much like it does on the front. Both default themes declare editor-styles support, but that support only tells WordPress how to treat the styles it is handed — it loads none, and nothing named any: theme assets are registered with toFrontend(), leaving the editor with theme.json’s variables and none of the rules built from them. The theme’s built stylesheets are read from its Vite manifest and passed to add_editor_style(), which is what reaches inside the editor iframe; enqueueing on enqueue_block_editor_assets lands in the admin page around it instead. Any theme declaring the support gets this, with no change of its own
  • Multisite is usable again. Bootstrap::rewriteNetworkUrl() is hooked to the network_site_url filter, which WordPress applies with a $scheme of null whenever no explicit scheme is requested — the common case — while the method declared a non-nullable string $scheme. Every request fatalled as soon as MULTISITE was enabled, the admin included. $path and $scheme now carry defaults matching the filter’s own signature; single-site installs never reach this path (#296)
  • pollora:install no longer leaves a site whose articles answer 404. It set the permalink structure and flushed, but the rule set WordPress can build mid-install is incomplete — taxonomies and post types are not all registered at that point — so flushing stored exactly that, and every single-post URL 404’d until something flushed again later. The option is dropped instead, letting WordPress rebuild it on the first request. This is the same reasoning completeWebInstall() already applied to the WordPress web installer; that hook is registered only outside the console, so the command-line install never went through it. Measured on a fresh install: 404 on an article before, 200 after
  • The missing-theme guidance page now reaches the case it was written for. It was wired only onto the exception handler, catching the View [home] not found that view() used to throw; since the skeleton stopped declaring WordPress routes the template hierarchy decides, nothing calls view(), nothing throws, and a site with no theme answered a bare 404 instead — the crash the page exists to replace, wearing a different status code. The request is now taken over on template_redirect, which WordPress fires before any template is chosen and which runWp() reaches before Laravel routes, alongside the existing handling for robots, favicons, feeds and trackbacks. The exception path stays for anything still routing through view(), and its registration no longer sits behind a guard that could skip the hook. The admin, AJAX and REST keep their normal responses: the admin is where the user goes to fix this
  • Blade templates now outrank the PHP stubs at a theme’s root. The root holds the files WordPress needs to consider the theme valid — index.php in particular, which ships as “Silence is golden” — and was registered as a view path ahead of resources/views, so locate() ranked its index.php above resources/views/index.blade.php. Every request the template hierarchy could not match to a more specific template (archives, search, custom post types, any single view without its own template) rendered that stub: HTTP 200 with an empty body and no error anywhere. Blade and PHP candidates are now collected separately so every Blade one wins, and view paths are registered in the order they are declared. A classic PHP template with no Blade equivalent is still used
  • Installing WordPress on a URL where a site already answers no longer dies with Class "WP_Http_Cookie" not found. wp_install() calls wp_remote_get() on the site URL and WP_Http builds a WP_Http_Cookie for every Set-Cookie header it gets back, but the install bootstrap loaded only part of the HTTP stack. It now loads the classes wp-settings.php loads, in its order. This affected reinstalls, migrations, and taking a domain back
  • wordpress.org no longer offers updates for the themes the project scaffolds. WordPress sends every installed theme to its update API keyed by the directory name, so a theme whose name matches a published slug is offered that unrelated theme — default, which pollora:install generates, matches one, and accepting the offer replaced the project’s theme with a download of the theme published there in 2005. Offers aimed at a theme in the project’s themes directory are moved to no_update. The Update URI header does not cover this on its own: WordPress consults the matching update_themes_{$hostname} filter only when wordpress.org returned nothing for that theme
  • A site installed through the WordPress web installer no longer answers 404 on every category, tag and date archive. The rewrite rules WordPress stores during its own install are incomplete — taxonomies and post types are not all registered at that point — and nothing regenerated them afterwards. The option is now dropped on wp_install, where the migrations already run, so WordPress rebuilds the rules on a later request
  • wp-admin no longer reports the active theme as missing while the front end renders it. get_stylesheet_directory() goes through the theme_root filter Pollora sets, but wp_get_theme() computes the root itself and no filter applies there. Registering Pollora’s themes directory replaced $wp_theme_directories instead of appending to it, and get_raw_theme_root() answers a hardcoded /themes whenever a single directory is registered, which wp_get_theme() then resolves against WP_CONTENT_DIR. WordPress’s own themes directory is kept alongside Pollora’s, and the stylesheet_root and template_root options now hold the directory containing the themes rather than the active theme’s own directory, which was one level too deep
  • A site installed through the WordPress web installer explains what to do instead of crashing. That path never runs pollora:install, so themes/ stays empty while the stylesheet option already points at WP_DEFAULT_THEME, and every front-end request died on View [home] not found. A missing view on a front-end request now renders a page naming the two theme templates and the command that scaffolds them, and wp-admin carries the same warning. It applies only while the site has no theme at all, so a headless install keeps working and a theme missing a single template still surfaces its real error

v13.32.0-beta.2 Pre-release

· View on GitHub

Added

  • pollora.dev as the project website and documentation: homepage and support.docs in composer.json (shown on Packagist), homepage in package.json, the README, and a Documentation link in the Pollora admin dashboard header
  • Gutenberg blocks render with Blade: a block.json "render": "file:./render.blade.php" renders through the view factory with $attributes, $content and $block, Blade components included. pollora:make:block now creates dynamic Blade blocks by default — their markup is not stored in post_content, so changing it never invalidates existing content — and --static creates a block with save.jsx
  • BlockRegistrar::registerDirectory() and registerBlock() accept an optional basePath, the Vite project root the asset entry points are resolved from; it defaults to the closest directory holding a vite.config file, or else the one containing resources/

Changed

  • Blocks live in resources/views/blocks/{slug} instead of resources/blocks/{slug}, next to the Blade views, for pollora:make:block, its BlocksServiceProvider stub and the Vite entries it generates. See Migrating from resources/blocks
  • pollora:make:block limits Vite full reloads under resources/views to Blade files: laravel-vite-plugin’s refreshPaths and a resources/views/** glob would reload the page on every block JSX change instead of hot-replacing it
  • BREAKING for custom BlockRegistrarInterface implementations: both methods gained the optional ?string $basePath = null parameter
  • Relicensed under MIT, like the other Pollora packages: composer.json and the README declared GPL-2.0-or-later, and the only license files were the 0BSD/WTFPL texts inherited from the original WordPress integration by Jordan Doyle, now credited in the README
  • Development against WordPress 7.1 stubs (php-stubs/wordpress-stubs ^7.1); patches/wordpress-stubs.patch regenerated for them and reduced to the __() → __wp() rename. Its wp_mail() hunk no longer matched the stubs (WordPress added an $embeds parameter) and renamed a function nothing overrides
  • mockery/mockery ^1.6.11 in development

Deprecated

  • Blocks in resources/blocks are still registered, with a notice in the log, until Pollora v15. A theme or plugin blocks directory scans both locations, and a block present in both is taken from resources/views/blocks
  • pollora:make:block --dynamic, now the default

Fixed

  • A block render file outside the block directory is no longer included: WordPress built its own render callback from block.json whenever Pollora’s check rejected the path
  • Blocks built with vite build get their view script and stylesheets again: the Vite entries generated by pollora:make:block only listed each block’s index script, so viewScript, style and editorStyle were missing from the production manifest. Running the command again in a configured theme or plugin rewrites the entries
  • pollora:make:block adds wordpressPlugin() to the Vite plugins on first use: it imported the plugin, then skipped adding it because the name was already in the file
  • The Artisan commands renamed to the Laravel colon convention in v13.32.0-beta answer to their previous names again. The rename left no alias, so a project whose composer.json still ran pollora:env-setup in post-autoload-dump failed composer install right after upgrading. Kept as aliases: pollora:env-setup, pollora:make-theme, pollora:delete-theme, pollora:make-plugin, pollora:make-block, pollora:make-model, pollora:make-action, pollora:make-filter, pollora:make-posttype, pollora:make-taxonomy, pollora:make-wp-cli
  • pollora:install pointed to a non-existent wp:env-setup command when the database connection failed
  • Asset containers no longer share one Vite configuration. Each ViteManager reconfigured Laravel’s Vite singleton in place, so the last container set up — typically the theme — imposed its hot file on every other one: with the theme on npm run dev and a plugin built for production, the plugin’s assets resolved as built but were enqueued as hot, and the block editor crashed with forceFullUrl(): Argument #1 ($path) must be of type string, array given. The getAssetUrls macro likewise read the build directory of the last manager registered instead of its own, and the Vite client tag was rendered from the unconfigured singleton
  • pollora:install completes without interaction (--no-interaction, CI, piped stdin). It called pollora:make:theme without a name, which failed with Not enough arguments (missing: "name") after WordPress was installed; it now generates the default theme from pollora/theme-default — the theme WordPress activates on install — or the one named by the new --theme option
  • pollora:install runs its migrations with --force and fails when they fail. In production, migrate asks for confirmation and cancels when it cannot prompt, yet the install reported Migration completed successfully: a non-interactive production install left the sessions and cache tables missing and every page answered 500
  • Commands using PromptsForMissingOption (pollora:make:theme, pollora:make:plugin) apply their prompt defaults when they cannot prompt, instead of leaving the options empty: a theme generated without interaction had a style.css with no author, URI, description or version

Removed

  • patches/mockery-php84-nullable.patch — Mockery fixed the PHP 8.4 implicit nullable deprecations upstream, so the patch no longer applied and printed Could not apply patch! on every composer install, including in projects built on the skeleton. patches/wordpress-core.patch stays: it is what lets pollora/helper-overrider declare __()

v13.32.0-beta Pre-release

· View on GitHub

Pollora now follows Laravel’s version numbers: this release requires Laravel 13.32.

Added

  • Translation diagnostics in pollora:status and the admin dashboard — reports whether the __() override is the one actually installed, alongside the WordPress and Laravel locales. laravel/framework declares __() behind the same function_exists() guard as pollora/helper-overrider and Composer emits autoload.files in dependency order, so whichever loads first wins; when Laravel wins nothing errors, WordPress catalogues simply stop resolving and every core, theme and plugin string silently renders untranslated. Detection compares the file declaring __() against the one declaring pollora_translation_resolver() — they ship in the same helpers.php, so matching paths prove the package won the race whatever the install layout
  • WordPress Abilities API support through the new pollora/abilities package (requires WordPress 6.9)
    • Ability facade for fluent declaration: Ability::define('acme/get-posts')->description(…)->category(…)->can(…)->using(…)
    • #[Ability] attribute on classes implementing AbilityHandler, discovered automatically
    • Ability::category() for the categories abilities file under, declared for you when an ability names one nobody registered
    • Behaviour annotations (reads/creates/updates/deletes) published to AI clients as readOnlyHint, destructiveHint and idempotentHint
    • Typed SchemaBuilder for input and output JSON Schema, and a defensive Input reader
    • Permission checks receive the same input as the body, allowing per-object capability checks; they default to refusing
  • #[SkipDiscovery] attribute to exclude classes from the discovery engine
    • Classes annotated with #[SkipDiscovery] are completely invisible to all discoveries — no reflection loaded, no attributes scanned
    • except parameter for selective exclusion: #[SkipDiscovery(except: [HookDiscovery::class])] skips all discoveries except the listed ones
  • Publishable config/discovery.php with skip_classes and skip_paths arrays for config-level exclusions (third-party packages, vendor classes)
  • DiscoveryRegistrar — automatically registers Discovery classes from the service container, eliminating manual addDiscovery() calls in ServiceProviders
  • pollora_register() helper with ModuleType enum for simplified theme/plugin registration
    • Themes: pollora_register(ModuleType::Theme) — 3 lines in functions.php, auto-detects theme name and path
    • Plugins: pollora_register(ModuleType::Plugin, 'my-plugin', __DIR__) — replaces manual PluginRegistrar wiring
  • #[Ajax] attribute for declarative AJAX action registration with security-by-default
  • Discovery system performance benchmark suite (tests/Benchmark/) with realistic attribute complexity and multi-location DDD scenarios
  • Public method caching in ReflectionCache::getPublicMethods() — avoids redundant getMethods(IS_PUBLIC) reflection calls

Changed

  • BREAKING: requires Laravel 13.32 (illuminate/* ^13.32). The framework version now tracks the Laravel release it targets, hence the jump from v13.4.3
  • BREAKING: Pollora\Services\Translater now requires its $domain argument. The old 'wordpress' default existed only to pair with the wordpress. key prefix that pollora/helper-overrider 1.2.0 removed — it prefixed every lookup as __('wordpress.'.$value) and relied on the resolver stripping it before the gettext call, so with 1.2.0 installed it silently returned values untranslated. Nothing in the framework used it (Sidebar and Menus both pass 'sidebars'/'menus' explicitly); pass __($value, 'default') if you want WordPress’s core catalogue. Two latent bugs went with it: the prefix was stripped with an unanchored str_replace(), so a value such as 'Go to menus.example' came back as 'Go to example', and translateItem() was typed string under declare(strict_types=1), so a wildcard translate(['*']) over a config array holding an int or bool raised a TypeError
  • BREAKING: Ajax module extracted to pollora/ajax package
  • BREAKING: Option module extracted to pollora/option package
  • BREAKING: Hook domain and adapters extracted to pollora/hook package
  • Domain layer paths restored for hexagonal architecture consistency
  • Public API types exposed for Theme and Option modules
  • All manual addDiscovery() calls removed from ServiceProviders — replaced by DiscoveryRegistrar auto-registration
  • Empty boot() methods removed from LaravelPluginModule and LaravelThemeModule (Rector RemoveParentDelegatingClassMethodRector)
  • Artisan command signatures renamed to Laravel colon convention (e.g. pollora:make:theme)
  • DiscoveryItems::all() optimized — replaced spread-in-loop with array_merge
  • Redundant reflection pre-loading removed from DiscoveryEngine

Fixed

  • WordPressHeaders middleware now respects the DONOTCACHEPAGE constant set by WooCommerce and compatible cache plugins, preventing public cache headers on cart, checkout, and account pages
  • __() no longer fatals on a translation with replacements whose key is absent from the current locale. __('Shipping :brand', ['brand' => 'Test']) raised TypeError: Cannot access offset of type array in isset or empty whenever Lang::has() answered no: the guard normalised only the empty array to the default text domain, so a non-empty replacement array travelled into WordPress’s translate() as the domain and reached isset($l10n[$domain]). It hit every __($key, [...]) call whose key was not in the locale’s catalogue — typically a site whose sources and language are both English, where no en_US.json exists at all, taking down every string with a placeholder. Requires pollora/helper-overrider 1.2, which routes on the caller’s intent — a string second argument is a WordPress text domain, a non-empty array is a Laravel call — instead of on whether Laravel happens to hold the key. The same release fixes an unguarded Lang::has() that raised A facade root has not been set. wherever __() runs before or without an application, a wordpress. prefix strip that rewrote the substring anywhere it appeared (__('Go to wordpress.org') returned Go to org), Laravel group keys returning an array to callers typed against WordPress’s string, and a missing base-language locale fallback that kept a fr_FR site from ever reading lang/fr.json
  • The Loop and Query facade aliases no longer crash a page that uses them. Both were still declared in extra.laravel.aliases after their facade classes had been deleted — Loop since 9a50cac (2026-04-21), Query since 9c0cb42 (2024-08-05). Laravel registers an alias without checking its target and only calls class_alias() the first time the short name is used, so the package installed and booted cleanly and then threw Class "Pollora\Support\Facades\Loop" not found on whichever page happened to call it. Present in every v13.4.x release. See the migration note below for what replaces Loop.
  • PluginManagerTest isolation — real Container with dynamic WP_PLUGIN_DIR replaces fragile ContainerInterface mock
  • WooCommerce hooks test updated for ComingSoonHandler dependency
  • PHPStan dead catches widened, redundant @return $this docblocks removed
  • OptionService namespace aligned with extracted package
  • GitHub releases are created again on tag push — the Deploy workflow lacked contents: write; suffixed tags (-beta, -rc.1) are now published as pre-releases

Removed

  • The Loop and Query entries in extra.laravel.aliases, whose facade classes no longer exist

Migrating from Loop

Loop:: was removed in 9a50cac as a breaking change, but the alias stayed behind and the removal was never written down — so a project only found out when a page 500’d. This is that note, late.

In Blade, the replacement is Sage Directives (log1x/sage-directives, registered by PolloraServiceProvider): @title, @content, @excerpt, @permalink, @published, @posts and the rest.

In PHP, Pollora\View\Loop was only ever a proxy over WordPress functions. The full mapping, read off the deleted source rather than reconstructed:

RemovedReplacement
Loop::id()get_the_ID()
Loop::title($post)get_the_title($post)
Loop::author()get_the_author()
Loop::authorMeta($field, $userId)get_the_author_meta($field, $userId)
Loop::content($moreText, $stripTeaser)apply_filters('the_content', get_the_content($moreText, $stripTeaser)), then str_replace(']]>', ']]&gt;', …)
Loop::excerpt($post)apply_filters('the_excerpt', get_the_excerpt($post))
Loop::thumbnail($size, $attr, $post)get_the_post_thumbnail($post, $size, $attr)
Loop::thumbnailUrl($size, $icon)wp_get_attachment_image_src(get_post_thumbnail_id(), $size, $icon)[0] ?? null
Loop::link($post, $leavename)get_permalink($post, $leavename)
Loop::category($id)get_the_category($id)
Loop::tags($id)get_the_tags($id) ?: []
Loop::terms($taxonomy, $post)get_the_terms($post, $taxonomy) ?: []
Loop::date($format, $post)get_the_date($format, $post)
Loop::postClass($class, $postId)'class="'.implode(' ', get_post_class($class, $postId)).'"'
Loop::nextPage($label, $maxPage)get_next_posts_link($label, $maxPage)
Loop::previousPage($label)get_previous_posts_link($label)
Loop::paginate($args)paginate_links($args)

Four of these are not straight renames, and a blind substitution breaks them:

  • thumbnail() and terms() reorder their arguments — the post moves from last to first — and both defaulted the post to the one in the loop.
  • postClass() returned a ready-made class="…" attribute string, where get_post_class() returns an array.
  • content() and excerpt() applied the_content / the_excerpt themselves; dropping the filter silently strips whatever other plugins add there.
  • tags() and terms() normalised false to an empty array.

Query:: has no replacement to document — the facade was already gone before v13, and the alias outlived it by two years.

  • Unfinished Admin Pages module
  • Manual addDiscovery() boot logic in HookServiceProvider, PostTypeServiceProvider, TaxonomyServiceProvider, WpRestAttributeServiceProvider, SchedulerDiscoveryServiceProvider
  • RegisterScheduleDiscoveryUseCase (superseded by DiscoveryRegistrar)