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
hrefattribute really is updated in the database. Only the rendered output differs, becauseLinkitFilter::process()overwriteshreffrom the UUID at render time, keeping only#fragmentand?queryfrom the stored value. - Revision diff reports no change at all. With the Diff module's default
visual_inlinelayout, 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
- 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.
- View the saved source and confirm the anchor tag carries
data-entity-type,data-entity-uuidanddata-entity-substitution. - Edit the node. Click the existing link, then click Edit link in the balloon.
- Clear the URL field manually and type an internal path by hand — for example
/media/7265— without clicking any autocomplete suggestion. - Click Insert, then save the node.
- View the saved source. Observe that
hrefis now/media/7265, butdata-entity-uuidstill holds the original entity's UUID. - View the rendered page. Editors expect the link to point at
/media/7265. In reality, it points at the original entity's aliased URL. - 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: canonicalenabled 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.
| Comment | File | Size | Author |
|---|---|---|---|
| #8 | interdiff-1-2.txt | 1.67 KB | kevin w |
| #8 | 3615946-detect-value-change-2.patch | 2.74 KB | kevin w |
Issue fork linkit-3615946
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
Comment #4
tribekk commentedOpened 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
Comment #5
csakiistvanComment #6
csakiistvanEnvironment
Prerequisites
defaultLinkit profile with theentity:nodematcher andcanonicalsubstitution./alpha-target/beta-targetThe rendered page source is misleading on its own, because
LinkitFilter::process()rewriteshreffrom the stored UUID at render time. Compare the stored value against the rendered output instead:Steps
ddev drush crdata-entity-type,data-entity-uuid(the ALPHA article's UUID) anddata-entity-substitution./node/<BETA NID>— without clicking any autocomplete suggestion. Confirm the change and save the node.Expected results
data-entity-type,data-entity-uuidanddata-entity-substitutionattributes are removed, so the renderedhrefmatches the URL the editor typed.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
LinkitDialogCKEditor5Testchanges 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.
Comment #7
kevin w commentedThanks 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.
Comment #8
kevin w commentedNoticed a bug in my previous patch. Here is a new version with interdiff.
Comment #9
idebr commented@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.
Comment #11
kevin w commented@idebr Thanks for the suggestion! I've created another MR 184 with 183's test updates.
Comment #12
aaron.silber@kevin confirming your MR 184 (when index.js is rebuilt locally) resolves this issue for us.
Comment #13
csakiistvan@aaronsilber-0 may I move forward to Needs Review?
Comment #15
odenscRebased MR !184 against latest 7.x.
Comment #16
aaron.silber@csakiistvan Yes I think so.
Comment #17
csakiistvanComment #18
csakiistvan✅ Tested and works — applied MR !184, rebuilt
js/build/linkit.jslocally 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 thedata-entity-*attributes, and the rendered page links to/node/5instead of node A's alias. Manually typed URLs no longer keep the previous entity's metadata.