Skip to content

fix(content-loop): keep editor preview links from navigating away - #1016

Open
wil-gerken wants to merge 8 commits into
mainfrom
feat/nppm-3241-inert-preview
Open

fix(content-loop): keep editor preview links from navigating away#1016
wil-gerken wants to merge 8 commits into
mainfrom
feat/nppm-3241-inert-preview

Conversation

@wil-gerken

@wil-gerken wil-gerken commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

All Submissions:

Changes proposed in this Pull Request:

The problem

Some links in Content Loop and Content Carousel previews could still navigate the editor away from the post being edited:

  • Image captions and credits.
  • Links in excerpt and full-content previews.
  • The article meta footer.
  • Links added by filters or site-specific code.

This could leave the front-end page loaded inside the editing canvas, with the admin bar across the top. Clicking Edit there opens a second editor inside the first.

The fix

Previous fixes disabled known links one group at a time, and in two different ways: a placeholder href="#" written into the block's own markup, and separately a rewrite of the REST payload the preview renders (#992). This change handles navigation in one place instead: a capture-phase click handler on each block's preview wrapper stops unmodified link clicks from navigating the editor, regardless of where the link came from.

That now covers:

  • Headlines, thumbnails, categories, tag labels, and read-more links.
  • Author names and avatars, including custom bylines.
  • Sponsor logos and sponsor bylines.
  • Image captions and credits.
  • Links in the post body, in both excerpt and full-content previews.
  • The article meta footer.
  • Anything added by a filter.

Because navigation is handled at the container, those links can keep their real destinations instead of using href="#". That restores two useful behaviors:

  • Cmd-click, Ctrl-click, and middle-click open a link in a new tab without leaving the editor. This does not apply to the Content Carousel, where .swiper-wrapper { pointer-events: none } already stops mouse clicks from reaching links inside slides. The handler covers keyboard activation there.
  • Hovering a link shows its real destination.

One browser-controlled path remains: right-clicking a link and choosing Open Link can still navigate the editor, because that action does not fire a page click event.

Links with no destination now render unlinked, matching the front end. Newspack_Blocks::get_post_link() returns false for a post type that is not public and has no external URL, and sponsor_url is empty whenever a sponsor has no link set.

Ref NPPM-3241. Follows NPPM-3165 (#992).

How to test the changes in this Pull Request:

Set up the fixtures on main:

  1. Publish post A. Publish post B with a link to post A in its body. The link must be in the body, not the excerpt.
  2. Give post B a featured image. Fill in both Credit and Credit URL in the Attachment Details sidebar, alongside the image you just picked. The credit URL is what puts a link inside the caption.
  3. Publish a page containing this block, pasted in through Options → Code editor. Full content is required: it is what puts post B's body link in the preview.
<!-- wp:newspack-blocks/homepage-articles {"showExcerpt":false,"showFullContent":true,"postsToShow":3,"showImage":true,"showCaption":true,"showCredit":true,"showCategory":true,"showAuthor":true,"showAvatar":true,"showDate":true,"showReadMore":true} /-->

Reproduce the problem on main. Skip this half if you only have the branch checked out; verification starts at step 8.

  1. Edit the page. Click the block once to select it.
  2. Click the link to post A in the preview. The canvas loads the front end of post A, with the admin bar across the top.
  3. Click Edit in that admin bar. A second editor loads inside the canvas.
  4. View the page on the front end and note how the article previews look. This is the baseline for step 15.

Verify the fix on this branch:

  1. Edit the page, select the block, and click the link to post A. The canvas stays on the page being edited.
  2. Click the credit link in an image caption. Nothing navigates.
  3. Tab to a post title in the preview and press Enter. Nothing navigates.
  4. Hover that title. The status bar shows the post's permalink, not #.
  5. Cmd-click that title (Ctrl-click on Windows and Linux). The post opens in a new browser tab, and the tab holding the editor is unchanged.
  6. Middle-click the same title. The post opens in a new tab the same way.
  7. Add a Content Carousel block to the page. Click a slide title, then tab to a slide title and press Enter. Neither navigates, and the slider still responds to its arrow buttons.
  8. If the site already has sponsored posts or posts with tag labels, click those in the preview too. Neither navigates.
  9. View the page on the front end. It looks as it did in step 7, and each link opens its own destination.

Other information:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes, as applicable? A Jest suite covers the handler, and a PHP test covers the payload contract it relies on. End-to-end coverage is described under Technical details.
  • Have you successfully run tests with your changes locally? Jest, PHPUnit, typescript:check, PHPCS, and ESLint all pass.
Technical details — implementation, prior art, and deferred coverage

Why the wrapper, not each link

The preview renders anchors this block does not author: RawHTML for byline, avatars, categories, excerpt, full content, and the article meta footer, plus whatever the newspack_blocks_post_byline and newspack_blocks_categories filters inject. Handlers attached to the anchors this block builds cannot reach those, and neither could the per-field rewriting. A site-specific plugin that hooks newspack_blocks_categories and appends an anchor with a live URL is enough to defeat it, since neutralize_editor_links() ran on two payload fields and not that one.

WordPress core takes the same approach in its Latest Posts block: keep a real href, cancel the click. Core attaches the handler to each anchor, which works because its preview is entirely JSX. Ours also renders server-built and filter-injected markup, so the handler goes one level up.

Removed

Newspack_Blocks::neutralize_editor_links() and both call sites, and ten hardcoded href="#" anchors across edit.tsx, carousel/edit.js, shared/js/utils.js, and the primary-category builder in class-newspack-blocks-api.php. The jsx-a11y/anchor-is-valid disable came out of the three JavaScript files, since no placeholder anchors remain. .swiper-wrapper { pointer-events: none } is deliberately untouched: it quiets Swiper drag, not links.

Modifier guard

The handler returns early on metaKey, ctrlKey, shiftKey, and altKey, the same check link-intercepting routers use. One gap: the Super key on Windows and Linux sets metaKey while carrying no link behavior of its own, so in principle that chord navigates. In practice the operating system claims it before the click reaches the page, and React Router's isModifiedEvent carries the same edge case.

Front-end output is unchanged

newspack_blocks_format_byline(), newspack_blocks_format_avatars(), templates/article.php, and carousel/view.php are untouched. newspack_blocks_get_primary_category() runs only in posts_endpoint(); the front end builds its category markup through newspack_blocks_format_categories().

Deferred end-to-end coverage

Three payload tests asserting href="#" are gone. One test replaces them, asserting the payload carries the live author-archive URL, the contract the editor now relies on.

An end-to-end spec covering the canvas is written and verified: it passes against this fix and fails against a build with the handler removed. It is not in this PR. The nightly suite runs specs from main against a site on the stable release channel, so a spec expecting this fix would fail there every night until the fix reaches stable. The spec ships in a follow-up PR once the release carrying this change reaches that site.

Until then, one claim is unpinned: nothing in the shipping suite proves the handler is attached to the wrapper. Removing onClickCapture leaves every Jest and PHPUnit test green.

🤖 Generated with Claude Code

wil-gerken and others added 7 commits August 31, 2026 11:04
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates newspack-blocks editor previews for the Content Loop (Homepage Articles) and Content Carousel blocks so preview links keep their real destinations while preventing unmodified clicks from navigating the block editor canvas away from the post being edited.

Changes:

  • Introduces a capture-phase click handler (preventPreviewNavigation) and attaches it to preview wrappers to cancel unmodified link navigation.
  • Restores “real” href values in editor preview markup (titles, thumbnails, tag labels, sponsors, bylines/avatars), matching front-end output more closely.
  • Updates PHPUnit/Jest coverage to reflect the new contract: editor payloads carry live URLs; navigation prevention happens at the preview container.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
plugins/newspack-blocks/tests/test-homepage-posts-block.php Updates REST payload contract test to assert live author-archive URLs are preserved.
plugins/newspack-blocks/src/shared/js/utils.test.js Updates sponsor markup tests to expect real sponsor URLs and adds “no URL => no anchor” assertions.
plugins/newspack-blocks/src/shared/js/utils.js Updates sponsor formatter output to render real links when a sponsor URL exists.
plugins/newspack-blocks/src/shared/js/inert-preview.test.js Adds Jest coverage for the click-cancel predicate (anchors vs non-anchors, modified vs unmodified).
plugins/newspack-blocks/src/shared/js/inert-preview.js Adds shared capture handler to prevent editor-canvas navigation from preview links.
plugins/newspack-blocks/src/blocks/homepage-articles/edit.tsx Restores real href usage in preview markup and attaches onClickCapture handler to wrapper.
plugins/newspack-blocks/src/blocks/carousel/edit.js Restores real href usage in preview markup and attaches onClickCapture handler to wrapper.
plugins/newspack-blocks/includes/class-newspack-blocks-api.php Stops rewriting byline/avatar links to # and makes primary category link real when available.
Suppressed comments (1)

plugins/newspack-blocks/src/shared/js/utils.js:38

  • Same as formatSponsorLogos: sponsor.sponsor_url is used verbatim as an href, but the underlying meta is not URL-sanitized. Please ensure only safe URL schemes (and/or relative URLs) are allowed before rendering a clickable anchor in the editor.
				<span className="author" key={ sponsor.id }>
					{ sponsor.sponsor_url ? <a href={ sponsor.sponsor_url }>{ sponsor.sponsor_name }</a> : sponsor.sponsor_name }
				</span>,

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread plugins/newspack-blocks/includes/class-newspack-blocks-api.php Outdated
Comment thread plugins/newspack-blocks/src/shared/js/utils.js
Comment thread plugins/newspack-blocks/src/shared/js/inert-preview.js
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wil-gerken
wil-gerken marked this pull request as ready for review September 2, 2026 01:59
@wil-gerken
wil-gerken requested a review from a team as a code owner September 2, 2026 01:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants