Skip to content

fix(quant_search): publish a search page on every save, and suggest a better search box region - #270

Merged
stooit merged 5 commits into
2.xfrom
fix/search-page-republish
Nov 14, 2026
Merged

stooit merged 5 commits into
2.xfrom
fix/search-page-republish

Conversation

@stooit

@stooit stooit commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Bug A: saving a search page did not always republish it

Cause. Only the two forms published a search page: QuantSearchPageForm::save() (modules/quant_search/src/Form/QuantSearchPageForm.php:498 on 2.x) and CreateSearchPageForm::submitForm() (CreateSearchPageForm.php:296). The module had a delete hook but no insert or update hook. A page saved any other way (drush, a recipe, config:import, custom code) stayed stale on Quant until the next seed. Node saves do not have this gap: quant_node_update() publishes from the entity hook.

Reproduced on a Drupal 11.4 site with a Quant connection, with a listener that records and stops Quant events before they are sent: $page->save() dispatched nothing; a submit of the edit form dispatched one OUTPUT for the route.

Fix.

  • quant_search.module: the insert, update and delete hooks call quant_search.page_publisher (schedule() and scheduleDelete()). The forms no longer call the publisher.
  • SearchPagePublisher works like a node save:
    • Only when quant.settings:quant_enabled is on and the site has a token. Delete is guarded the same way now; before, it unpublished on every site. Under drush, CliDomainContext::initialize() runs before these checks, so a domain's settings and token decide.
    • After the request: the hooks register quant_shutdown, which runs publishPending(). A config import is complete before a page renders, and several saves of one page in a request publish once. Config import saves (isSyncing()) publish too.
    • Any error (\Throwable) of a page (facet registration, render, API) is logged and recorded in the push failures list. A router rebuild error is logged. Neither breaks a save or an import.
    • Not during installation or in maintenance mode: the change is logged and listed in the push failures list ("not sent during installation or maintenance mode"), so editors see it until the page is saved again or the site is seeded. If that record fails (for example, the state does not work yet during installation), the failure is logged and the save goes on. PushFailures::record() takes an optional reason for this.
    • The pending pages are static and the shutdown callback asks the container for the current service, so a container rebuild during a config import does not split or lose them.
    • The router is marked for rebuild on save and delete, and rebuilt before the publish.
  • A disabled page is unpublished. A deleted page is unpublished. A page that moves unpublishes its old address and sends a 301 redirect from the old address to the new one, as a changed node alias does. If another enabled page uses the old address now (two pages swapped addresses), the old address stays.
  • QuantSearchPageForm::validateForm() refuses an address that another search page uses, compared without slashes and case (QuantSearchPage::routeKey()), because Quant paths do not depend on case. QuantSearchPage::preSave() stores the route without spaces or slashes at either end (QuantSearchPage::normaliseRoute()) and keeps its case, because Drupal routes depend on case. The publisher compares routes the same way, and trims the previous route too, so a route stored as /search/ before this change unpublishes /search. A change of case only is not a move: nothing is unpublished or redirected.

Bug B: the search box defaulted to the header region

Cause. CreateSearchPageForm::buildBlockChoice() (CreateSearchPageForm.php:215 on 2.x) chose header when the theme has that region. In Olivero and themes based on it, the header region holds the logo. Olivero puts its own search box (olivero_search_form_wide) in secondary_menu, at the end of the main menu row.

Fix. CreateSearchPageForm::defaultRegion() suggests, in this order: secondary_menu; the region of an enabled core search box (search_form_block) in the theme; header; the first region. The region field says which region it suggests, links to Block layout, and says so when the Quant box will sit next to the Drupal search box. The search box choice shows only when the block module is installed (the module does not depend on it).

Tests

  • New tests/src/Functional/SearchPagePublishTest.php (16 tests; setUp() empties the publisher's static pending list):
    • a save in code publishes once, after the request, also after two saves;
    • disable unpublishes, enable publishes;
    • a route change unpublishes the old address and redirects it (301);
    • two pages that swap addresses keep both;
    • delete unpublishes;
    • a route is stored trimmed with its case kept;
    • a change of case only is not a move;
    • a route stored with slashes unpublishes the trimmed path;
    • a change in maintenance mode is listed as not sent;
    • with quant_enabled off, insert, update, disable and delete send nothing;
    • without a token, nothing is sent;
    • a config import publishes once, after the import;
    • a Quant API failure (500) breaks neither the save nor a config import, and is recorded;
    • the edit form publishes once, and refuses an address in use (also as /Search/);
    • the create form works without the block module and publishes once.
  • CreateSearchPageTest: the create form publishes once; three tests for the region suggestion (Olivero; a theme with an enabled core search box, where a disabled one does not count; neither).
  • tests/src/Traits/WaitsForQuantCallsTrait.php: the test web server answers before shutdown functions run, so form tests wait for the publish call, then 3 more seconds so that a late duplicate fails the once-checks. CreateSearchPageTest::tearDown() waits for the publish of each page the form created, so a publish never writes into a removed test site.
  • The fake Quant API records the Quant-Url header (unpublish), records calls under a lock (a publish after the response runs at the same time as the next test request), and can answer every call with a 500.
  • Run locally on Drupal 11.4 (SQLite): all quant_search tests pass. phpcs --standard=Drupal with the CI extensions: clean.

RELEASE-NOTES-2.3.1.md describes the change for site owners.

PR #268

This PR is based on 2.x. Both PRs change quant_search_quant_search_page_delete(). #268 adds the removal of the page's search boxes after the unpublish line, and this PR replaces that line with scheduleDelete($entity). Whichever merges second needs a small conflict resolution in that function: keep scheduleDelete($entity) and #268's block removal. The other files merge without conflicts.

Open points

  • Width: in Olivero, a full-width search box in secondary_menu shares the menu row. With a long main menu the row wraps and Olivero switches to the mobile menu, as it does for the box in header. On a test site with seven top-level items at 1920 px, the row needed about 8 px more. A narrower box or fewer menu items fixes it. Olivero's own search avoids this with a toggle button.
  • In the mobile menu, a box in secondary_menu is inside the menu drawer, as Olivero's own search is. A box in header stays on the top bar.
  • Multilingual: a page publishes at its route without a language prefix, as before. Translated addresses (/fr/search) are not published or withdrawn on save; a seed still covers them.
  • Domains: under drush the domain context comes from CliDomainContext (--uri). A save in a web request publishes to the project of the request's host, as node saves do. On a multi-domain site a search page is not tied to a domain, so it publishes to the project of whichever domain saved it.
  • drush updb and drush si --existing-config: installation is skipped (logged). An update hook that saves a page publishes after the update run, through the site's local webserver setting.
  • No status report check for two pages with the same route (a config import does not run form validation). The routes are normalised on save, and the publish keeps an address that another enabled page uses.
  • Pages saved before this change are not republished by the upgrade. Save them again, or seed.

… better search box region

Saving a search page published it only from the two forms, so a page saved
by drush, a recipe, a config import or code stayed stale on Quant until the
next seed. The entity insert and update hooks now call the publisher, like
node saves: a save publishes, a disable unpublishes, and a changed route
withdraws the old address. Delete already unpublished.

"Create a search page" placed the search box in the header region. In
Olivero that region holds the logo; Olivero keeps its own search box in the
secondary menu region. The form now suggests secondary_menu, then the region
of the theme's core search box, then the header, and names the suggestion.
…Quant is enabled

The search page hooks published during the save, ignored quant_enabled and
let a Quant API error break the save or a config import. They now schedule
the work in a shutdown function, as node saves do: it runs only with
quant_enabled on and a token, after the request (so after a config import),
once per page, never during installation or in maintenance mode, and a
Quant error is logged and recorded as a push failure. Delete is guarded the
same way.

A moved page redirects its old address to the new one, unless another page
uses the old address now. The edit form refuses an address in use. The
region suggestion counts only enabled core search boxes and says when the
box will sit next to one, and the box choice needs the block module.
… maintenance mode

The route of a search page is stored without slashes at either end and in
lower case, and the edit form compares routes the same way, because Quant
paths do not depend on case. A change of case only is not a move.

A change made during installation or in maintenance mode is listed with the
push failures, with its own reason, so editors see that it is not on Quant.
The publisher resolves the domain before it reads quant_enabled and the
token, catches every error per page and of the router rebuild, keeps the
pending pages across a container rebuild, and marks the router for rebuild
on delete. The fake API records calls under a lock.
…vious route

Drupal routes depend on case, so lowercasing the stored route broke links
to /Search on the site after the next save. The route is now stored trimmed
of spaces and slashes only, and routes are compared without case wherever
the address on Quant matters (the edit form, the old address of a moved
page, an address in use). The previous route is trimmed too, so a route
stored as /search/ unpublishes /search. A failure to list a change left in
maintenance mode or installation is logged and does not break the save.
@stooit
stooit merged commit 294a775 into 2.x Nov 14, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant