Place CiteCue page enhancements on the page you already have (tier 1) - #13
Merged
Conversation
The `/seo-head` response has carried a `body` half since Phase 3 R14 — the page-enhancement block, a collapsed facts-and-FAQ section composed from the customer's own answered facts for a page they already have. Until now no plugin declared `body_blocks`, so every WordPress site was sent `body: ''` and the feature reached Cloudflare Worker installs only. Declared and injected in one change, deliberately. The capability is what makes CiteCue start sending real block markup, so a release that announced it without placing it would put markup on a live customer page that nothing renders. The block rides the response already fetched for the head, so an enhanced page costs no extra request and inherits the rules that path already keeps: cache-only on the render, nothing fetched at all where injection is off. The two halves are otherwise independent — either can be empty while the other is not, and `refresh()` no longer reads an empty `head` as an empty answer, which would have thrown every body-only block away. Placed verbatim, and that is the contract rather than an oversight: the block is sanitized at composition and capped on the wire, and re-escaping it would print its tags as text while `wpautop` or the content filters would run a shortcode quoted inside an answer. The trust is put where it can be reasoned about instead — the size cap re-checked here, the marker check that refuses to place a second copy on a page that already carries one, and the last `</body>` rather than one quoted in a code sample. Existing connections need one reconnect and now say so. CiteCue records capabilities at connect time and withholds anything a plugin never announced, and the old prompt compared one setting, which had not changed — so without this the feature would have reached nobody who was already a customer, and nothing would have said why. Also asks for the site-wide identity baseline. This plugin prints no Organization node of its own, and the head merge drops CiteCue's JSON-LD outright when the page carries any, so it can never become a competing one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reconnect prompt had one message per metadata setting, and the capability drift added in the previous commit fires on neither of them: a site upgrading into page enhancements has not touched that setting, so it was told its metadata was not reaching CiteCue — a fault it would then go looking for and not find. The reason is now computed once and named, and the prompt says the one that applies. Consent is deliberately not among the reasons. Allowing content pushes gates the ingest endpoint and nothing on this path — enhancements arrive through the same authenticated /seo-head read as the metadata — so prompting for it here would be asking an administrator to grant write access to their site for a feature that does not use it. "Content from CiteCue" already reports that state where it belongs. Enhancements also get their own row in the status card, rather than being implied by the metadata row that no longer describes them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consent is about to become real on CiteCue's side: reconnecting with the box unticked clears the stored signing secret, and nothing tells this site when that happens. The switch here would go on reading "Accepted" over a channel with no secret behind it. Two reads close that, because they cover different moments. The plugin's own claim response is the first: it sent a secret and asked, and the wire contract omits the flag rather than sending false, so absence THERE is a definite "not ticked" rather than "not mentioned" — the one context where that inference is sound. The daily sync is the second, for a withdrawal made at CiteCue between reconnects, read off `contentPush` on the project entry `/config` already returns. Silence still never grants. `contentPush` being true does not switch pushes on: it says CiteCue holds a secret, which is not the same as this site agreeing to be written to, and flipping that from a remote read would grant write access to a site whose owner had refused it. The reconcile only ever closes. Absent is not false, either. The field is read with array_key_exists() rather than a truthiness test, because a CiteCue that predates it would otherwise look like one withdrawing consent and switch pushes off on every site at once. Everything ambiguous — a transport failure, a project list this site's key is not in — leaves the switch alone, since acting on nothing would revoke a working connection over a bad afternoon on the network. The daily read costs a request only where pushes are actually on, which the default is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both survive only as long as someone can see what they defend, and both defend against a change on CiteCue's side rather than anything in this file — which is exactly the shape that gets simplified away by a reader checking the current API and finding the branch unreachable. The absent-key guard is the dangerous one. `contentPush` is a required boolean on every project entry, so against today's API the branch genuinely cannot be reached and the test genuinely cannot fail. What it covers is the deployment that does not have the field at all — a rollback, a staging origin on an older build, a self-hosted app behind on releases — which no schema describes and which arrives without warning. Against one of those, a truthiness test reads every project as a withdrawal and switches content pushes off on every site at once. The revoke-only guard needed the opposite note. Now that a secret is stored only where the customer ticked the box, `contentPush: true` really does imply they consented once, so the argument for treating it as a grant is available in a way it was not before. It is still a fact about CiteCue's storage at one instant, not the administrator's standing decision about their own site, and they may have closed the switch here deliberately since. Comments and test docblocks only. No behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The banner and icon were generated into a temporary directory and lost to a cleanup, which is a poor place for a deliverable that took design iteration to arrive at. They now live in the repository, beside a script that rebuilds them. `.wordpress-org/` holds what WordPress.org serves above the readme; `assets-src/` holds the banner source and the build. Neither is part of the plugin — the directory serves them from the SVN `assets/` folder, which sits beside `trunk/` and is never installed on a site — so both are export-ignored and the zip guard now names them too. A lost export-ignore line should fail the build rather than ship page art to every install. The build renders with headless Chrome because it is the one thing reliably present that lays out real webfont text, and it inlines the faces as data URIs first: --screenshot does not wait for a font fetched over file://, so a relative @font-face renders the whole banner in a fallback with different metrics and reports success. It asserts the four output dimensions, since a banner at the wrong size is refused by the directory as a silently missing header rather than an error. docs/releasing.md writes down the SVN procedure, which existed only in a terminal scrollback and has now been reconstructed twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "already placed" check was a bare substring search, so a page quoting `<section data-citecue="page-enhancement">` in a code sample read as a page that already had one, and would have been denied its enhancement forever. This class already records that lesson about the head — a page that quotes markup contains whatever it quotes — and the body check had not learned it. Scoped to a real opening tag, which escaped markup cannot satisfy because the escaped form has no `<`, and to the whole attribute value rather than a prefix. The value boundary is not hypothetical: CiteCue composes the FAQ payload inside the block as `data-citecue="page-enhancement-faq"`, so a prefix match treats that sibling as the section marker, and the section is what delimits the region CiteCue owns. Both existing guarantees still hold — a real block is still detected however it is quoted, and a duplicate is still refused. Found by CodeRabbit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PHPUnit suite calls the injector directly or through a synthetic buffer. That covers the decisions but not what the decisions are for: whether the block reaches a browser at all, through a real theme, a real wp_head()/ wp_footer(), and whichever output-buffer mechanism the WordPress under test provides. On 6.9+ that is core's template enhancement filter, which nothing in the suite exercises end to end. `bin/local-rig.sh` stands up WordPress from the core the tests already vendor, symlinks the plugin in, and points it at a stub CiteCue through the plugin's own CITECUE_API_BASE override. `verify` asserts against rendered pages and tears down; `up` leaves it running to look at. Ten checks, and two of them are the ones this rig exists for. The FAQ payload is real JSON-LD in a real script element, so the block surviving intact is demonstrated rather than argued. And a page that quotes the marker in a code sample still gets its block, which is the CodeRabbit fix proven where it matters rather than in a regex test. The stub also logs what the plugin sent, so the Bearer token and channel header are asserted rather than assumed. bin/ is already export-ignored, so none of this can reach the plugin zip; the guard in build-plugin-zip.sh covers it either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… one
The rig could only talk to its own stub, which meant every assertion rested on
my reading of the contract rather than on the server's behaviour. It now takes
RIG_API_BASE / RIG_API_KEY / RIG_PUBLIC_KEY and skips the stub entirely, which
is also what the post-deploy acceptance test will need.
Run against a local build of the merged app, the real endpoint answers
`{"head":"","body":"<section data-citecue=\"page-enhancement\">…"}` — a
populated body with an empty head, for a project that has a block but no
enriched page. That is the exact case the previous `refresh()` discarded by
reading an empty head as an empty answer, so the fix is now demonstrated
against the server rather than against my guess about it.
Two checks had to learn what they were actually asserting:
The head half is now conditional on what the server sent, because the two
halves are independent and a project with a block and no enriched page is a
legitimate answer. Asserting a head unconditionally failed the plugin for the
server's correct behaviour. It reads the plugin's own cache for the answer
rather than lookup_url(), which derives from $_SERVER and is empty under CLI —
that returned '' and skipped the assertion instead of making it, in the mode
where it should have run.
The channel assertion is built with strtolower() rather than written as a
literal. phpcbf's prose sniff rewrites a bare "wordpress" into "WordPress",
which silently turned a passing check into one that failed against correct
behaviour. The plugin's own sources are unaffected — the header value is
lowercase in every one — but a test that phpcbf can invert is a test that will
be inverted again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI's coding-standards job failed on the rig I added: phpcs scans the whole tree, and `bin/rig/*` tripped 42 violations across ten sniffs. I did not catch it locally because I had been reading phpcs through `tail`, which shows the timing line and hides every error above it — the command was exiting 1 the whole time. Split by whether the sniff is saying something true about this code. Fixed rather than excused: the stub's missing @Package tag and file-comment spacing, an undocumented helper, an undocumented parameter, and two short ternaries. The helper was also called `wp_json_encode_fallback`, which reads as a WordPress function in a file that deliberately runs outside WordPress; it is now `citecue_rig_json` and says so. Waived, alongside `/tests/*` and for the same reason plus one: the rig is not shipped — bin/ is export-ignored and the zip guard refuses an archive containing it — and it is not plugin code. Prefixing a local `$url` in a test harness with `citecue_` would be ceremony that makes the harness harder to read without making anything safer. Four categories only: global prefixes, global overrides, file naming. Also closes a gap the failure exposed: `composer lint` never covered `bin/`, so a parse error in the rig would have reached CI as a phpcs crash rather than a syntax error. It covers it now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements tier 1 of the page-enhancements handoff: declares the
body_blocksandseo_head_baselinedelivery capabilities and injects/seo-head'sbodyblock immediately before</body>on ordinary page loads. Also closes a stranded-consent gap the app-side consent gate creates, and fixes a bug in the plugin's own reconnect prompt found while doing that.body_blocks/seo_head_baselinerideseo_head_enabled, built by oneCitecue_Settings::declared_capabilities()that the reconnect check also reads, so the two sets can't drift apart.</body>(not the first, to avoid a quoted one in a code sample), marker guard ondata-citecue="page-enhancement"(recognized however it's quoted), 32 KB cap re-checked locally, empty string is a no-op. Head and body are independent — either can inject while the other doesn't.refresh()no longer discards a body-only answer./seo-headreturns 200 when either half is non-empty; the old code read an emptyheadas a miss and would have thrown every body-only block away.seo_head_reconnect_reason()returnsenabled/disabled/capabilities/''instead of one boolean, so a site upgrading into this release (whose metadata setting hasn't moved) isn't told its metadata isn't reaching CiteCue — a fault an admin would go hunting for and not find. Page enhancements got their own status-card row.ingestthere is a definite withholding, since the wire contract omits rather than sends false) and a daily reconcile againstGET /config's per-projectcontentPushfield (gated oningest_enabledbeing on, so it costs nothing on the default-off majority). Revoke-only —contentPush: truenever grants; that decision stays on the connect screen..wordpress-org/,assets-src/build-assets.sh), both export-ignored, with the zip guard checking for them so a lost ignore line fails the build instead of shipping page art to every install.docs/releasing.mddocuments the SVN publish procedure.Why this isn't ready to release yet
Declaring
body_blockschanges the app's block-ownership resolution (resolveBlockDeliveryOwner) fromworkertocmsfor any connection that also has a stored ingest secret — which this plugin always sends. Onmainin the app repo today, that flips ownership for every new page-enhancement block to a CMS push path this plugin does not implement (upsert_block/remove_block), while/seo-head'sbodygoes empty for exactly those blocks.The app side is adding a separate
block_pushcapability (currently on a branch, not yet onmain, not yet deployed) so thatbody_blocksalone keeps ownership atworker— which is what this plugin needs. This branch does not declareblock_push, so once that capability split ships it will behave correctly; until then, merging this PR tomainand releasing to WordPress.org would strand every newly-created enhancement.Confirmed via a live cross-session exchange with the engineer working the app side (citecue/citecue_app): the capability split is agreed and being implemented, but production has no automated deploy — it's manual — so "merged" won't mean "live." Do not release to WordPress.org until that's confirmed deployed.
Test plan
composer test)phpcs/phpcbfclean against WPCSbin/build-plugin-zip.sh— version strings agree, package is exactly the 16 runtime files, no dev files leakedassets-src/build-assets.sh— regenerates the four directory-asset PNGs at exact pixel dimensionsdata-citecue="page-enhancement"section renders and disappears on revert — blocked on the app-sideblock_pushdeploy above🤖 Generated with Claude Code