@@ -393,7 +393,19 @@ public async Task DeleteUrlsFromCacheAsync(IEnumerable<Guid> documentKeysEnumera
393393 return GetTopMostRootKey ( isDraft , culture ) ;
394394 }
395395
396- // Otherwise we have to find the root items (or child of the first root when hideTopLevelNodeFromPath is true) and follow the url segments in them to get to correct document key
396+ // Special case for all top level nodes except the first (that will have /)
397+ if ( runnerKey is null && urlSegments . Length == 1 && hideTopLevelNodeFromPath is true )
398+ {
399+ IEnumerable < Guid > rootKeys = GetKeysInRoot ( false , isDraft , culture ) ;
400+ Guid ? rootKeyWithUrlSegment = GetChildWithUrlSegment ( rootKeys , urlSegments . First ( ) , culture , isDraft ) ;
401+
402+ if ( rootKeyWithUrlSegment is not null )
403+ {
404+ return rootKeyWithUrlSegment ;
405+ }
406+ }
407+
408+ // Otherwise we have to find the root items (or child of the roots when hideTopLevelNodeFromPath is true) and follow the url segments in them to get to correct document key
397409 for ( var index = 0 ; index < urlSegments . Length ; index ++ )
398410 {
399411 var urlSegment = urlSegments [ index ] ;
@@ -447,7 +459,7 @@ public string GetLegacyRouteFormat(Guid documentKey, string? culture, bool isDra
447459 var cultureOrDefault = string . IsNullOrWhiteSpace ( culture ) is false ? culture : _languageService . GetDefaultIsoCodeAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
448460
449461 Guid [ ] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid [ ] ?? ancestorsOrSelfKeys . ToArray ( ) ;
450- IDictionary < Guid , Domain ? > ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray . ToDictionary ( x => x , ancestorKey =>
462+ ILookup < Guid , Domain ? > ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray . ToLookup ( x => x , ancestorKey =>
451463 {
452464 Attempt < int > idAttempt = _idKeyMap . GetIdForKey ( ancestorKey , UmbracoObjectTypes . Document ) ;
453465
@@ -466,13 +478,11 @@ public string GetLegacyRouteFormat(Guid documentKey, string? culture, bool isDra
466478
467479 foreach ( Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray )
468480 {
469- if ( ancestorOrSelfKeyToDomains . TryGetValue ( ancestorOrSelfKey , out Domain ? domain ) )
481+ var domains = ancestorOrSelfKeyToDomains [ ancestorOrSelfKey ] . WhereNotNull ( ) ;
482+ if ( domains . Any ( ) )
470483 {
471- if ( domain is not null )
472- {
473- foundDomain = domain ;
474- break ;
475- }
484+ foundDomain = domains . First ( ) ; // What todo here that is better?
485+ break ;
476486 }
477487
478488 if ( _cache . TryGetValue ( CreateCacheKey ( ancestorOrSelfKey , cultureOrDefault , isDraft ) , out PublishedDocumentUrlSegment ? publishedDocumentUrlSegment ) )
@@ -528,7 +538,7 @@ public async Task<IEnumerable<UrlInfo>> ListUrlsAsync(Guid contentKey)
528538 var cultures = languages . ToDictionary ( x=> x . IsoCode ) ;
529539
530540 Guid [ ] ancestorsOrSelfKeysArray = ancestorsOrSelfKeys as Guid [ ] ?? ancestorsOrSelfKeys . ToArray ( ) ;
531- Dictionary < Guid , Task < Dictionary < string , Domain > > > ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray . ToDictionary ( x => x , async ancestorKey =>
541+ Dictionary < Guid , Task < ILookup < string , Domain > > > ancestorOrSelfKeyToDomains = ancestorsOrSelfKeysArray . ToDictionary ( x => x , async ancestorKey =>
532542 {
533543 Attempt < int > idAttempt = _idKeyMap . GetIdForKey ( ancestorKey , UmbracoObjectTypes . Document ) ;
534544
@@ -537,70 +547,119 @@ public async Task<IEnumerable<UrlInfo>> ListUrlsAsync(Guid contentKey)
537547 return null ;
538548 }
539549 IEnumerable < Domain > domains = _domainCacheService . GetAssigned ( idAttempt . Result , false ) ;
540- return domains . ToDictionary ( x => x . Culture ! ) ;
550+ return domains . ToLookup ( x => x . Culture ! ) ;
541551 } ) ! ;
542552
543553 foreach ( ( string culture , ILanguage language ) in cultures )
544554 {
545- var urlSegments = new List < string > ( ) ;
546- Domain ? foundDomain = null ;
555+ var urlSegments = new List < string > ( ) ;
556+ List < Domain ? > foundDomains = new List < Domain ? > ( ) ;
547557
548- var hasUrlInCulture = true ;
549- foreach ( Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray )
550- {
551- if ( ancestorOrSelfKeyToDomains . TryGetValue ( ancestorOrSelfKey , out Task < Dictionary < string , Domain > > ? domainDictionaryTask ) )
558+ var hasUrlInCulture = true ;
559+ foreach ( Guid ancestorOrSelfKey in ancestorsOrSelfKeysArray )
560+ {
561+ var domainLookup = await ancestorOrSelfKeyToDomains [ ancestorOrSelfKey ] ;
562+ if ( domainLookup . Any ( ) )
552563 {
553- Dictionary < string , Domain > domainDictionary = await domainDictionaryTask ;
554- if ( domainDictionary . TryGetValue ( culture , out Domain ? domain ) )
564+ var domains = domainLookup [ culture ] ;
565+ foreach ( Domain domain in domains )
555566 {
556- Attempt < Guid > domainKeyAttempt = _idKeyMap . GetKeyForId ( domain . ContentId , UmbracoObjectTypes . Document ) ;
567+ Attempt < Guid > domainKeyAttempt =
568+ _idKeyMap . GetKeyForId ( domain . ContentId , UmbracoObjectTypes . Document ) ;
557569 if ( domainKeyAttempt . Success )
558570 {
559571 if ( _publishStatusQueryService . IsDocumentPublished ( domainKeyAttempt . Result , culture ) )
560572 {
561- foundDomain = domain ;
562- break ;
573+ foundDomains . Add ( domain ) ;
563574 }
564575 }
565576 }
577+
578+ if ( foundDomains . Any ( ) )
579+ {
580+ break ;
581+ }
566582 }
567583
568- if ( _cache . TryGetValue ( CreateCacheKey ( ancestorOrSelfKey , culture , false ) , out PublishedDocumentUrlSegment ? publishedDocumentUrlSegment ) )
584+ if ( _cache . TryGetValue (
585+ CreateCacheKey ( ancestorOrSelfKey , culture , false ) ,
586+ out PublishedDocumentUrlSegment ? publishedDocumentUrlSegment ) )
569587 {
570588 urlSegments . Add ( publishedDocumentUrlSegment . UrlSegment ) ;
571589 }
572590 else
573591 {
574592 hasUrlInCulture = false ;
575593 }
576- }
594+ }
595+
596+ //If we did not find a domain and this is not the default language, then the content is not routable
597+ if ( foundDomains . Any ( ) is false && language . IsDefault is false )
598+ {
599+ continue ;
600+ }
577601
578- //If we did not find a domain and this is not the default language, then the content is not routable
579- if ( foundDomain is null && language . IsDefault is false )
580- {
581- continue ;
582- }
583602
584- var isRootFirstItem = GetTopMostRootKey ( false , culture ) == ancestorsOrSelfKeysArray . Last ( ) ;
603+ var isRootFirstItem = GetTopMostRootKey ( false , culture ) == ancestorsOrSelfKeysArray . Last ( ) ;
604+
605+ var leftToRight = _globalSettings . ForceCombineUrlPathLeftToRight
606+ || CultureInfo . GetCultureInfo ( culture ) . TextInfo . IsRightToLeft is false ;
607+ if ( leftToRight )
608+ {
609+ urlSegments . Reverse ( ) ;
610+ }
611+
612+ // If no domain was found, we need to add a null domain to the list to make sure we check for no domains.
613+ if ( foundDomains . Any ( ) is false )
614+ {
615+ foundDomains . Add ( null ) ;
616+ }
617+
618+ foreach ( Domain ? foundDomain in foundDomains )
619+ {
620+ var foundUrl = GetFullUrl ( isRootFirstItem , urlSegments , foundDomain , leftToRight ) ;
621+
622+ if ( foundDomain is not null )
623+ {
624+ // if we found a domain, it should be safe to show url
625+ result . Add ( new UrlInfo (
626+ text : foundUrl ,
627+ isUrl : hasUrlInCulture ,
628+ culture : culture
629+ ) ) ;
630+ }
631+ else
632+ {
633+ // otherwise we need to ensure that no other page has the same url
634+ // e.g. a site with two roots that both have a child with the same name
635+ Guid ? documentKeyByRoute = GetDocumentKeyByRoute ( foundUrl , culture , foundDomain ? . ContentId , false ) ;
636+ if ( contentKey . Equals ( documentKeyByRoute ) )
637+ {
638+ result . Add ( new UrlInfo (
639+ text : foundUrl ,
640+ isUrl : hasUrlInCulture ,
641+ culture : culture
642+ ) ) ;
643+ }
644+ else
645+ {
646+ result . Add ( new UrlInfo (
647+ text : "Conflict: Other page has the same url" ,
648+ isUrl : false ,
649+ culture : culture
650+ ) ) ;
651+ }
652+ }
585653
586- var leftToRight = _globalSettings . ForceCombineUrlPathLeftToRight
587- || CultureInfo . GetCultureInfo ( culture ) . TextInfo . IsRightToLeft is false ;
588- if ( leftToRight )
589- {
590- urlSegments . Reverse ( ) ;
591- }
592654
593- result . Add ( new UrlInfo (
594- text : GetFullUrl ( isRootFirstItem , urlSegments , foundDomain , leftToRight ) ,
595- isUrl : hasUrlInCulture ,
596- culture : culture
597- ) ) ;
598655
656+ }
599657 }
600658
601659 return result ;
602660 }
603661
662+
604663 private string GetFullUrl ( bool isRootFirstItem , List < string > segments , Domain ? foundDomain , bool leftToRight )
605664 {
606665 var urlSegments = new List < string > ( segments ) ;
@@ -610,7 +669,7 @@ private string GetFullUrl(bool isRootFirstItem, List<string> segments, Domain? f
610669 return foundDomain . Name . EnsureEndsWith ( "/" ) + string . Join ( '/' , urlSegments ) ;
611670 }
612671
613- var hideTopLevel = _globalSettings . HideTopLevelNodeFromPath && isRootFirstItem ;
672+ var hideTopLevel = HideTopLevel ( _globalSettings . HideTopLevelNodeFromPath , isRootFirstItem , urlSegments ) ;
614673 if ( leftToRight )
615674 {
616675 return '/' + string . Join ( '/' , urlSegments . Skip ( hideTopLevel ? 1 : 0 ) ) ;
@@ -624,6 +683,21 @@ private string GetFullUrl(bool isRootFirstItem, List<string> segments, Domain? f
624683 return '/' + string . Join ( '/' , urlSegments ) ;
625684 }
626685
686+ private bool HideTopLevel ( bool hideTopLevelNodeFromPath , bool isRootFirstItem , List < string > urlSegments )
687+ {
688+ if ( hideTopLevelNodeFromPath is false )
689+ {
690+ return false ;
691+ }
692+
693+ if ( isRootFirstItem is false && urlSegments . Count == 1 )
694+ {
695+ return false ;
696+ }
697+
698+ return true ;
699+ }
700+
627701 public async Task CreateOrUpdateUrlSegmentsWithDescendantsAsync ( Guid key )
628702 {
629703 var id = _idKeyMap . GetIdForKey ( key , UmbracoObjectTypes . Document ) . Result ;
@@ -646,7 +720,7 @@ public async Task CreateOrUpdateUrlSegmentsAsync(Guid key)
646720 }
647721 }
648722
649- private IEnumerable < Guid > GetKeysInRoot ( bool addFirstLevelChildren , bool isDraft , string culture )
723+ private IEnumerable < Guid > GetKeysInRoot ( bool considerFirstLevelAsRoot , bool isDraft , string culture )
650724 {
651725 if ( _documentNavigationQueryService . TryGetRootKeys ( out IEnumerable < Guid > rootKeysEnumerable ) is false )
652726 {
@@ -655,13 +729,10 @@ private IEnumerable<Guid> GetKeysInRoot(bool addFirstLevelChildren, bool isDraft
655729
656730 IEnumerable < Guid > rootKeys = rootKeysEnumerable as Guid [ ] ?? rootKeysEnumerable . ToArray ( ) ;
657731
658- foreach ( Guid rootKey in rootKeys )
732+ if ( considerFirstLevelAsRoot )
659733 {
660- yield return rootKey ;
661- }
734+ yield return rootKeys . First ( ) ;
662735
663- if ( addFirstLevelChildren )
664- {
665736 foreach ( Guid rootKey in rootKeys )
666737 {
667738 if ( isDraft is false && IsContentPublished ( rootKey , culture ) is false )
@@ -677,6 +748,13 @@ private IEnumerable<Guid> GetKeysInRoot(bool addFirstLevelChildren, bool isDraft
677748 }
678749 }
679750 }
751+ else
752+ {
753+ foreach ( Guid rootKey in rootKeys )
754+ {
755+ yield return rootKey ;
756+ }
757+ }
680758
681759 }
682760
@@ -710,23 +788,28 @@ private IEnumerable<Guid> GetChildKeys(Guid documentKey)
710788 return Enumerable . Empty < Guid > ( ) ;
711789 }
712790
713- /// <summary>
714- /// Gets the top most root key.
715- /// </summary>
716- /// <returns>The top most root key.</returns>
717- private Guid ? GetTopMostRootKey ( bool isDraft , string culture )
791+ private IEnumerable < Guid > GetRootKeys ( bool isDraft , string culture )
718792 {
719793 if ( _documentNavigationQueryService . TryGetRootKeys ( out IEnumerable < Guid > rootKeys ) )
720794 {
721795 foreach ( Guid rootKey in rootKeys )
722796 {
723797 if ( isDraft || IsContentPublished ( rootKey , culture ) )
724798 {
725- return rootKey ;
799+ yield return rootKey ;
726800 }
727801 }
728802 }
729- return null ;
803+ }
804+
805+
806+ /// <summary>
807+ /// Gets the top most root key.
808+ /// </summary>
809+ /// <returns>The top most root key.</returns>
810+ private Guid ? GetTopMostRootKey ( bool isDraft , string culture )
811+ {
812+ return GetRootKeys ( isDraft , culture ) . Cast < Guid ? > ( ) . FirstOrDefault ( ) ;
730813 }
731814
732815 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
0 commit comments