From 63ae29a1aa6df0b482b453e62fdaa35267e13aed Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Wed, 22 Oct 2025 09:48:55 +0200
Subject: [PATCH] Preview: Fixes a potential issue where the preview URL could
be a different backoffice host (#20591)
* hotfix: ensures that local urls stay relative so we land up on the correct backoffice host that the user initiated the preview session from originally
* feat: since ensureAbsoluteUrl is never supplied anymore, we can remove the parameter altogether
* Remove unused dependency
* Expose IsExternal for URLs
* feat: adds localize controller
* chore: generates api models
* feat: marks the internal preview default url as relative, so that the `` tag is taken into consideration - that way the URL will open on whatever host is active
* Remove IsExternal from the API again
* regenerate types
---------
Co-authored-by: kjac
---
.../Factories/DocumentUrlFactory.cs | 13 ++++---------
src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs | 4 ++--
.../workspace/document-workspace.context.ts | 8 +++++++-
3 files changed, 13 insertions(+), 12 deletions(-)
diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs
index 998eb80873..53bd255a2b 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs
@@ -16,7 +16,6 @@ public class DocumentUrlFactory : IDocumentUrlFactory
private readonly UrlProviderCollection _urlProviders;
private readonly IPreviewService _previewService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
- private readonly IAbsoluteUrlBuilder _absoluteUrlBuilder;
private readonly ILogger _logger;
public DocumentUrlFactory(
@@ -24,14 +23,12 @@ public class DocumentUrlFactory : IDocumentUrlFactory
UrlProviderCollection urlProviders,
IPreviewService previewService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
- IAbsoluteUrlBuilder absoluteUrlBuilder,
ILogger logger)
{
_publishedUrlInfoProvider = publishedUrlInfoProvider;
_urlProviders = urlProviders;
_previewService = previewService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
- _absoluteUrlBuilder = absoluteUrlBuilder;
_logger = logger;
}
@@ -39,7 +36,7 @@ public class DocumentUrlFactory : IDocumentUrlFactory
{
ISet urlInfos = await _publishedUrlInfoProvider.GetAllAsync(content);
return urlInfos
- .Select(urlInfo => CreateDocumentUrlInfo(urlInfo, false))
+ .Select(CreateDocumentUrlInfo)
.ToArray();
}
@@ -89,18 +86,16 @@ public class DocumentUrlFactory : IDocumentUrlFactory
}
}
- return CreateDocumentUrlInfo(previewUrlInfo, previewUrlInfo.IsExternal is false);
+ return CreateDocumentUrlInfo(previewUrlInfo);
}
- private DocumentUrlInfo CreateDocumentUrlInfo(UrlInfo urlInfo, bool ensureAbsoluteUrl)
+ private DocumentUrlInfo CreateDocumentUrlInfo(UrlInfo urlInfo)
{
var url = urlInfo.Url?.ToString();
return new DocumentUrlInfo
{
Culture = urlInfo.Culture,
- Url = ensureAbsoluteUrl && url is not null
- ? _absoluteUrlBuilder.ToAbsoluteUrl(url).ToString()
- : url,
+ Url = url,
Message = urlInfo.Message,
Provider = urlInfo.Provider,
};
diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
index c4385bb5ad..e6d4def79e 100644
--- a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
+++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs
@@ -144,7 +144,7 @@ public class NewDefaultUrlProvider : IUrlProvider
public Task GetPreviewUrlAsync(IContent content, string? culture, string? segment)
=> Task.FromResult(
UrlInfo.AsUrl(
- $"/{Constants.System.UmbracoPathSegment}/preview?id={content.Key}&culture={culture}&segment={segment}",
+ $"preview?id={content.Key}&culture={culture}&segment={segment}",
Alias,
culture,
isExternal: false));
@@ -182,7 +182,7 @@ public class NewDefaultUrlProvider : IUrlProvider
// We have the published content now, so we can check if the culture is published, and thus avoid the DB hit.
string route;
var isDraft = _umbracoContextAccessor.GetRequiredUmbracoContext().InPreviewMode;
- if(isDraft is false && string.IsNullOrWhiteSpace(culture) is false && content.Cultures.Any() && content.IsInvariantOrHasCulture(culture) is false)
+ if (isDraft is false && string.IsNullOrWhiteSpace(culture) is false && content.Cultures.Any() && content.IsInvariantOrHasCulture(culture) is false)
{
route = "#";
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts
index 9eef3cde1b..0d199d874c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts
@@ -40,6 +40,7 @@ import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/documen
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import type { UmbVariantPropertyGuardRule } from '@umbraco-cms/backoffice/property';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
+import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
type ContentModel = UmbDocumentDetailModel;
type ContentTypeModel = UmbDocumentTypeDetailModel;
@@ -70,6 +71,7 @@ export class UmbDocumentWorkspaceContext
#isTrashedContext = new UmbIsTrashedEntityContext(this);
#documentSegmentRepository = new UmbDocumentSegmentRepository(this);
#actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE;
+ #localize = new UmbLocalizationController(this);
constructor(host: UmbControllerHost) {
super(host, {
@@ -350,7 +352,11 @@ export class UmbDocumentWorkspaceContext
}
if (previewUrlData.message) {
- umbPeekError(this._host, { color: 'danger', headline: 'Preview error', message: previewUrlData.message });
+ umbPeekError(this._host, {
+ color: 'danger',
+ headline: this.#localize.term('general_preview'),
+ message: previewUrlData.message,
+ });
throw new Error(previewUrlData.message);
}
}