Skip to content

Redirect Tracking: Fix segment change detection and optimise descendant traversal (closes #22082)#22091

Merged
Migaroez merged 7 commits intomainfrom
v17/improvement/optimize-redirect-tracking
Mar 15, 2026
Merged

Redirect Tracking: Fix segment change detection and optimise descendant traversal (closes #22082)#22091
Migaroez merged 7 commits intomainfrom
v17/improvement/optimize-redirect-tracking

Conversation

@AndyButland
Copy link
Copy Markdown
Contributor

@AndyButland AndyButland commented Mar 11, 2026

Prerequisites

  • I have added steps to test this contribution in the description below

Description

This PR addresses #22082 by optimising the redirect tracker.

As part of that work, I've found and fixed a couple of bugs that would otherwise prevent URL redirects from being created when content is renamed.

Changes

1. Optimisation: Skip descendant traversal when URL segment is unchanged

The main problem I feel is that we are currently checking to create redirects for the node and it's descendants on every save and publish. In the vast majority of these cases, no redirects are needed and so this is wasteful. On the other hand, we can't just simply check if the node name or umbracoUrlName property has changed, as whilst out of the box that's all that impacts the generated URL segment. URL segment providers are extensible, so we can't know what factors might go into calculating the URL segment.

To resolve this I've used the segment providers themselves to find the new segment, compared it to the old, and if they are the same, early return to not carry out the unnecessary traversal of descendent nodes. If the segment is unchanged, no descendant URLs can have changed either, so the entire traversal is skipped.

This is implemented via a new IUrlSegmentProvider.HasUrlSegmentChanged method with a default interface implementation, replacing a long-standing TODO comment. The intention is that this a permanent default implementation, so only providers that need to update this behaviour need to implement it.

The IRedirectTracker.StoreOldRoute method has a new bool isMove overload so the handler can distinguish publishes from moves — moves will still always traverse descendants since the parent path changes.

2. Optimisation: Batch deduplication of already-processed descendants

When multiple content items are published in a batch and a parent's traversal already processed a child, the child's subsequent StoreOldRoute call is skipped by checking if its ID is already in the oldRoutes dictionary.

3. Bug fix: DocumentUrlService.GetUrlSegment returned null for invariant content

GetUrlSegment and GetUrlSegments returned null/empty when called with an empty culture string for invariant content. Invariant content is stored with a null language ID in the cache, but TryGetLanguageIdFromCulture("") fails — the code previously returned early at that point. Now it falls through to the invariant (null language ID) cache lookup. TryGetPrimaryUrlSegment had the same issue and is fixed identically.

4. Bug fix: DefaultUrlSegmentProvider ignored published parameter in name fallback

When content has no umbracoUrlName property, GetUrlSegmentSource falls back to the content name. The fallback had a special case: if document.Edited is true and a published name exists, always return the published name — regardless of the published parameter. This meant GetUrlSegment(content, published: false, culture) returned the old published name instead of the current draft name.

This broke redirect tracking: HasUrlSegmentChanged compares the currently-published segment against what the segment will be after publishing (draft values), but both sides returned the published name, so no change was ever detected for invariant content.

The fix adds published && to the condition so the published-name preference only applies when published: true is requested. When published: false, the current (draft) name is returned — consistent with how the umbracoUrlName property path already behaved.

Testing

Automated

The various unit and integration tests added in this PR, plus existing ones for these classes, should pass.

Manual

Invariant content — rename and publish

  1. Create a document type with no culture variation (invariant)
  2. Create and publish a content node (e.g. "Original Name")
  3. Verify URL works: /original-name
  4. Rename the node to "New Name" and publish
  5. Expected: A 301 redirect is created from /original-name/new-name
  6. Verify in Content → URL Redirects that the redirect appears
  7. Browse to /original-name — should redirect to /new-name

Variant content — rename and publish

  1. Create a document type with culture variation enabled
  2. Create and publish a content node with at least one culture (e.g. "English Page" in en-US)
  3. Rename the en-US variant to "Updated English Page" and publish
  4. Expected: A 301 redirect is created from the old URL to the new URL
  5. Verify in Settings → URL Redirects

Move operation — verify descendants are tracked

  1. Create a parent node with a child node, both published
  2. Note the child's URL (e.g. /parent/child)
  3. Move the parent under a different node
  4. Expected: Redirects are created for both the parent and child URLs

No-change publish — verify no unnecessary work

  1. Publish a content node without changing its name
  2. Verify no new redirects are created
  3. Put a break point in RedirectTracker (line 89) and verify that the method early returns to avoid the unnecessary work.

Documentation

We should document MayAffectDescendantSegments on IUrlSegmentProvider.

…s and avoiding descendant traversal when there has been no change to the node's URL segment.
Copilot AI review requested due to automatic review settings March 11, 2026 09:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 improves redirect tracking reliability/performance during publish operations by detecting URL-segment changes (to avoid unnecessary descendant traversal) and fixes invariant-culture URL segment lookups so redirects are correctly created when invariant content is renamed.

Changes:

  • Add URL-segment change detection for publishes (with an isMove flag to preserve always-traverse semantics for moves) and batch de-duplication during redirect route capture.
  • Fix invariant-content segment lookups in DocumentUrlService when culture is "" (and related invariant fallbacks).
  • Fix DefaultUrlSegmentProvider to respect the published parameter when falling back to the content name, plus add/extend tests.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
uui-range-slider-issue.md Adds documentation about a uui-range-slider minGap=0 issue (appears unrelated to this PR’s redirect-tracking focus).
tests/Umbraco.Tests.UnitTests/Umbraco.Core/Strings/DefaultUrlSegmentProviderTests.cs New unit coverage for draft vs published name fallback behavior.
tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/DocumentUrlServiceTests.cs Adds tests for invariant segment resolution when culture is empty or when culture-specific entries are missing.
tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Routing/RedirectTrackerTests.cs Extends integration coverage for publish/move traversal behavior, batch de-duplication, and provider override scenarios.
src/Umbraco.Infrastructure/Routing/RedirectTrackingHandler.cs Passes isMove context into redirect tracking during move vs publish notifications.
src/Umbraco.Infrastructure/Routing/RedirectTracker.cs Implements segment-change gating for publishes and batch de-duplication; wires in URL segment providers + document URL service.
src/Umbraco.Core/Strings/IUrlSegmentProvider.cs Adds HasUrlSegmentChanged default interface method for consistent change detection across providers.
src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs Fixes name fallback to honor the published flag (draft vs published segment source).
src/Umbraco.Core/Services/DocumentUrlService.cs Fixes invariant fallback behavior in GetUrlSegment/GetUrlSegments/TryGetPrimaryUrlSegment when culture can’t be resolved.
src/Umbraco.Core/Routing/IRedirectTracker.cs Adds StoreOldRoute(..., isMove) overload and obsoletes the older overload.

@AndyButland
Copy link
Copy Markdown
Contributor Author

AndyButland commented Mar 11, 2026

We've discussed an an edge case where the segment-unchanged optimization could break custom IUrlSegmentProvider implementations. It may seem unlikely, but does show a flaw in the approach.

Scenario

- Node1 (type1) has propertyA and propertyB
  - Node2 (type2) is a child of Node1

A custom segment provider uses propertyA to compute Node1's segment, but uses propertyB on the ancestor (Node1) to compute Node2's segment.

When propertyB on Node1 changes, Node2's URL segment changes — but Node1's own segment is unchanged. The optimization would incorrectly skip descendant traversal, missing the redirect for Node2.

Proposed Solution

Added IUrlSegmentProvider.MayAffectDescendantSegments(IContentBase content) with a default of false. The skip condition now requires all the following to be true:

  1. Not a move operation
  2. Content's own URL segment is unchanged
  3. No registered provider reports that this content may affect descendant segments

Custom providers that derive descendant segments from ancestor data (or external sources) can override this, either returning true unconditionally, or using logic (e.g. checking the content type, level or property values) to limit traversal to affected subtrees. The built-in provider returns false (the default), so the optimization applies in the common case.

@AndyButland AndyButland added the status/needs-docs Requires new or updated documentation label Mar 11, 2026
@AndyButland AndyButland requested a review from Migaroez March 13, 2026 06:06
Copy link
Copy Markdown
Contributor

@Migaroez Migaroez left a comment

Choose a reason for hiding this comment

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

LGTM and covers all the edgecases i can think off.

@Migaroez Migaroez merged commit 7363183 into main Mar 15, 2026
26 of 27 checks passed
@Migaroez Migaroez deleted the v17/improvement/optimize-redirect-tracking branch March 15, 2026 15:24
AndyButland added a commit that referenced this pull request Mar 16, 2026
…nt traversal (#22091)

* Optimise redirect tracker by avoiding re-producing of descendant nodes and avoiding descendant traversal when there has been no change to the node's URL segment.

* Delete inadvertently added file

* Allow URL segment providers to ensure descendent traversal if needed.

* Pushed missing files.

* Refactors to reduce large method code smells.
@AndyButland AndyButland changed the title Redirect Tracking: Fix segment change detection and optimise descendant traversal Redirect Tracking: Fix segment change detection and optimise descendant traversal (closes #22082) Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/17.3.0 status/needs-docs Requires new or updated documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants