Do not rewrite fragment-only URLs on import - #263
Conversation
A reference like `#section` points within the document that contains it. It has no host and no path, so there is no origin to migrate. wp_rewrite_urls() resolved it against the base URL first, which made it look like a child of the site being imported from, and it was then rewritten against the new home: #section becomes /#section #add-to-calendar becomes /#add-to-calendar # becomes / (fragment dropped entirely) An in-page anchor therefore turns into a link to a different page, and blocks whose saved markup contains such an href fail validation after an import because the serialized attribute no longer matches what the block expects. Sites have been working around this by disabling URL rewriting altogether via the wp_import_options filter, which is a heavy price for one class of URL. Skips those references before the mapping is consulted. The guard reads the raw URL rather than the parsed one, because parsing is what loses the distinction: by the time a fragment has been resolved against the base it is indistinguishable from a same-site link. Scoped deliberately to references that are *only* a fragment. A URL that carries a fragment alongside a host or path is still rewritten, and still keeps its fragment. Adds regression tests covering fragment-only hrefs in markup and in block attributes, plus the two cases that must keep working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Flagging the red checks so they don't get misread as fallout from this change: they are pre-existing and unrelated to the diff. Every failing job dies in
Composer blocks advisory-affected packages by default now, and the pinned PHPUnit range is entirely within that advisory, so dependency resolution fails outright.
The failing job names line up too ( Meanwhile everything that did manage to install and run on this PR passed: all three E2E shards, plus Happy to open a separate PR unblocking CI if that would help. The options are roughly: allow the advisory through |
The problem
A reference like
#sectionpoints within the document that contains it. It has no host and no path, so there is no origin to migrate.wp_rewrite_urls()resolves every URL against the base URL before consulting the mapping. For a fragment-only reference that resolution produceshttps://old-site.com/current-page#section, which then matchesis_child_url_of()and gets rewritten against the new home:<a href="#section"><a href="/#section"><a href="#add-to-calendar"><a href="/#add-to-calendar"><a href="#"><a href="/">The last one is the worst: the fragment is dropped entirely, so an in-page anchor becomes a link to the site root.
Why it matters
Beyond the broken links, this breaks block validation. Any block whose saved markup contains a fragment-only
href(accordions, tab and anchor navigation, "jump to" links, and blocks that use#as a JavaScript-handled placeholder) no longer matches itssave()output after an import, and the editor shows "This block contains unexpected or invalid content."The workaround people land on is disabling URL rewriting altogether:
That is a heavy price for one class of URL, and it gives up the migration for every genuine link in the content.
The fix
Skip fragment-only references before the mapping is consulted.
The guard deliberately reads
get_raw_url()rather than the parsed URL, because parsing is what destroys the distinction: once#sectionhas been resolved against the base it is indistinguishable from an ordinary same-site link, which is exactly why the current code rewrites it.Scoped to references that are only a fragment. A URL carrying a fragment alongside a host or path still rewrites, and still keeps its fragment:
Tests
phpunit/tests/rewrite-urls.phpcovers fragment-only hrefs in both markup and block attributes, plus the two cases that must keep working (absolute URL, absolute URL carrying a fragment).Verified they fail before the change and pass after:
The two controls passing in both columns is the part that matters: the guard does not over-correct.
Notes
components/DataLiberation/URL/functions.php), vendored here undersrc/php-toolkit/. Happy to open the matching PR there so the two do not diverge, just say the word.<a href="?filter=all">currently becomes<a href="/?filter=all">, which likewise moves the link from the current page to the site root. I left it alone to keep this change to the reported behavior, but it may be worth the same treatment.phpcs --standard=phpcs.xml.distis clean on both files. I could not run the PHPUnit suite locally (the pinned PHPUnit predates my PHP version, as CONTRIBUTING.md notes), so those run on CI.