Problem/motivation

When an editor edits an existing Linkit-created link and types a new URL by hand instead of choosing an autocomplete suggestion (like changing /media/3852 to /media/9833), the data-entity-type, data-entity-uuid and data-entity-substitution attributes from the previous link are retained. It seems like LinkitFilter then regenerates the href from the stale UUID on every render, so the published page continues to point at the old href.

This fails silently. The editor sees their new URL in the editing form and in the saved source and only the rendered page indicates that anything is wrong.

Two things make it especially easy to miss:

  • The saved markup looks correct. The href attribute really is updated in the database. Only the rendered output differs, because LinkitFilter::process() overwrites href from the UUID at render time, keeping only #fragment and ?query from the stored value.
  • Revision diff reports no change at all. With the Diff module's default visual_inline layout, both revisions are rendered through a view mode before comparison, so both resolve the same stale UUID to the same href and the comparison reports no difference. An editor verifying their own change through the revision UI is told nothing happened. Our editors initially reported that our moderation workflow was broken due to the confusion.

The result is that any link an editor has repointed by typing, rather than by removing and recreating, has been serving the old target.

Steps to reproduce

  1. On a text format with the Linkit filter enabled, use CKEditor 5 to create a link by choosing an entity from the Linkit autocomplete suggestions. Save the node.
  2. View the saved source and confirm the anchor tag carries data-entity-type, data-entity-uuid and data-entity-substitution.
  3. Edit the node. Click the existing link, then click Edit link in the balloon.
  4. Clear the URL field manually and type an internal path by hand — for example /media/7265 — without clicking any autocomplete suggestion.
  5. Click Insert, then save the node.
  6. View the saved source. Observe that href is now /media/7265, but data-entity-uuid still holds the original entity's UUID.
  7. View the rendered page. Editors expect the link to point at /media/7265. In reality, it points at the original entity's aliased URL.
  8. Compare the two revisions using the Diff module's default visual layout. Expected: the href change is listed. Actual: no changes are reported.

Only unlinking the text completely and then re-linking it produces the expected result

Tested using;

  • Linkit 7.0.16
  • Drupal core 11.4.4
  • CKEditor 5 ~47.6.2 as shipped in core
  • media_substitution: canonical enabled on the affected text format.

Proposed resolution

Gate the clearing on whether the user chose an autocomplete suggestion, rather than on the shape of the typed URL.

Issue fork linkit-3615946

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

aaron.silber created an issue. See original summary.

tribekk made their first commit to this issue’s fork.

tribekk’s picture

Status: Active » Needs review

Opened MR !183.

Manual edits to a Linkit-created internal URL now clear entity metadata, while autocomplete selections and unchanged links retain it. The regression test updates an entity link to a manually entered internal path and confirms the data attributes are removed.

Checked locally:
- corepack yarn build
- php -l tests/src/FunctionalJavascript/LinkitDialogCKEditor5Test.php
- composer validate --no-check-publish
- git diff --check

csakiistvan’s picture

Assigned: Unassigned » csakiistvan
csakiistvan’s picture

Assigned: csakiistvan » Unassigned
Status: Needs review » Reviewed & tested by the community

Environment

  • Drupal: 11.4.4
  • PHP: 8.5.5
  • Database: MariaDB 10.11.16
  • DDEV: v1.25.2
  • Linkit: 7.x-dev (0a84c12)
  • Browser: Chrome

Prerequisites

  • Enable the Linkit filter and the Linkit CKEditor 5 plugin on the Full HTML text format, using the default Linkit profile with the entity:node matcher and canonical substitution.
  • Create two target articles with distinct URL aliases, so the difference between the stored and the rendered link target is visible:
    • "Linkit target ALPHA" with alias /alpha-target
    • "Linkit target BETA" with alias /beta-target
  • Create a third article with a Body field using the Full HTML format, containing a paragraph with some link text.

The rendered page source is misleading on its own, because LinkitFilter::process() rewrites href from the stored UUID at render time. Compare the stored value against the rendered output instead:

ddev drush php:eval '
$n = \Drupal::entityTypeManager()->getStorage("node")->loadUnchanged(<NID>);
print "STORED: " . $n->body->value . "\n";
$b = \Drupal::entityTypeManager()->getViewBuilder("node")->view($n, "default");
if (preg_match("#<a [^>]*>my link</a>#", (string) \Drupal::service("renderer")->renderInIsolation($b), $m)) print "RENDERED: $m[0]\n";
'

Steps

  1. Apply the fix from MR !183: the CKEditor 5 plugin now tracks whether the URL was picked from an autocomplete suggestion, resets that state when the link form opens or when the URL field is typed into, and clears the Linkit attributes when the URL was entered manually.
  2. Rebuild caches: ddev drush cr
  3. Edit the third article. Select the link text, open the link balloon, type part of the ALPHA article's title and click the autocomplete suggestion. Save the node.
  4. Confirm the stored markup carries data-entity-type, data-entity-uuid (the ALPHA article's UUID) and data-entity-substitution.
  5. Edit the node again. Click the existing link, click Edit link in the balloon, clear the URL field and type an internal path by hand — for example /node/<BETA NID> — without clicking any autocomplete suggestion. Confirm the change and save the node.
  6. Compare the stored and the rendered markup, and open the rendered page to check where the link actually points.
  7. Edit the link once more, this time picking the BETA article from the autocomplete suggestions, and save.
  8. Compare the stored and the rendered markup again.

Expected results

  • After a manual URL edit, the data-entity-type, data-entity-uuid and data-entity-substitution attributes are removed, so the rendered href matches the URL the editor typed.
  • After an autocomplete selection, the attributes are still written, with the newly selected entity's UUID, and the rendered link resolves to that entity's alias.

Actual results

As expected. Before the fix, the manual URL edit stored <a href="/node/20" data-entity-type="node" data-entity-uuid="<ALPHA UUID>" data-entity-substitution="canonical">, while the rendered output was <a href="/alpha-target" data-entity-uuid="<ALPHA UUID>" title="Linkit target ALPHA"> — the published page kept pointing at the previous target, and the rendered page source looked unchanged across the edit, which is exactly what makes the bug easy to miss.

After applying MR !183, the same manual edit stored and rendered <a href="/node/20">my link</a> with no leftover entity attributes. Re-selecting the BETA article from the autocomplete suggestions produced <a href="/beta-target" data-entity-type="node" data-entity-uuid="<BETA UUID>" data-entity-substitution="canonical" title="Linkit target BETA">, so metadata is still written for genuine autocomplete selections.

Note: the LinkitDialogCKEditor5Test changes in the merge request could not be executed in this environment, since its Selenium version is not compatible with the Mink driver used by Drupal's FunctionalJavascript tests. The verification above was done manually in the browser; the test coverage is left to the pipeline.


Testing produced with the assistance of an LLM.

kevin w’s picture

StatusFileSize
new2.26 KB

Thanks for MR !183 — I hit the same underlying bug from a different angle and wanted to share a variant that also covers it.

We use the Bookmark feature from the module "ckeditor5_plugin_pack". It supports adding and linking to URI fragment in the same page like "#map". But we noticed that if a user create a link pointing to an entity first and then switch it to a URI fragment (bookmark), it simply append the URI fragment to the previous entity URI instead of clearing the entity URI.

I tried the MR !183 but realized that it detects a manual edit while CKEditor 5 Plugin Pack's Bookmark feature sets the field's value programmatically when a bookmark is selected, which never fires a native input event. So all the data attributes stay the same.

Rather than trying to enumerate every possible way the field's value can change (typing, this provider, some future provider), the attached patch compares the href actually being submitted against a baseline captured when the link form opens (this._initialHref = linkitInput.value, set alongside the existing _autocompleteSelection = undefined reset).

Patch attached against 7.0.16 (source only — js/build/linkit.js isn't included since I assume a maintainer would want to rebuild it as part of merging). Happy to also open this as its own MR if that's preferred over folding it into !183.

kevin w’s picture

StatusFileSize
new2.74 KB
new1.67 KB

Noticed a bug in my previous patch. Here is a new version with interdiff.

idebr’s picture

Status: Reviewed & tested by the community » Needs work

@kevin w your approach seems more robust. Can you create a MR and include the test coverage from MR 183, so both use cases are tested automatically?

Don't worry about the build file. It might conflict at some point, but can be regenerated from source once the branch is complete.

kevin w’s picture

@idebr Thanks for the suggestion! I've created another MR 184 with 183's test updates.

aaron.silber’s picture

@kevin confirming your MR 184 (when index.js is rebuilt locally) resolves this issue for us.

csakiistvan’s picture

@aaronsilber-0 may I move forward to Needs Review?

odensc made their first commit to this issue’s fork.

odensc’s picture

Rebased MR !184 against latest 7.x.

aaron.silber’s picture

@csakiistvan Yes I think so.

csakiistvan’s picture

Assigned: Unassigned » csakiistvan
Status: Needs work » Needs review
csakiistvan’s picture

Assigned: csakiistvan » Unassigned

✅ Tested and works — applied MR !184, rebuilt js/build/linkit.js locally and caches, then on a node whose Linkit link pointed at node A replaced the URL with a manually typed /node/5 (no autocomplete) and clicked Update: the saved markup is <a href="/node/5">the target</a> without the data-entity-* attributes, and the rendered page links to /node/5 instead of node A's alias. Manually typed URLs no longer keep the previous entity's metadata.