Skip to content

Conversation

@kjac
Copy link
Contributor

@kjac kjac commented Aug 28, 2025

Prerequisites

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

Description

This PR is part the effort to create a "new" backoffice replacement of the custom preview URLs concept from the "old" backoffice.

Moving forward, the URL providers will be responsible for generating all preview URLs - both internal and external ones.

To that end:

  • The IUrlProvider interface has been extended with the GetPreviewUrlAsync() method.
  • The IUrlProvider interface now must provide a unique Alias, which couples all URLs (all UrlInfo return values, including non-preview URLs) to the provider.
  • The UrlInfo class has been revamped to support URLs more gracefully, including the coupling to the provider alias.

All of this makes the PR breaking.

How will this all work?

All of this is somewhat work in progress - this is the current proposal.

We will introduce a new backoffice extension for URL providers. At this time, the extension is primarily targeting the preview URLs, but we do envision it could apply elsewhere - for example, for grouping links in the "info" section, or provide dedicated UI for URLs provided by a specific provider, or...

If more than one provider is registered for generating preview URLs, the "Save and preview" button will feature a pop-up with the additional providers for the content editor to choose from (it will likely become possible to determine which provider should be triggered by default "Save and preview" button action).

When the content editor hits "Save and preview" (for any provider, including the default/core URL provider), the backoffice client will:

  1. Save (or create) the document.
  2. Query the chosen URL provider for a preview URL using the new /document/{id]/preview-url endpoint.
    • This can (will) be a contextual preview URL, because the client must pass the active culture and segment alongside the ID of the document.
  3. The URL provider can respond with a preview URL and/or a "message".
  4. If a preview URL is returned, a new browser tab is launched with this URL.
  5. If a message is returned, the URL provider is for some reason unable to generate a preview URL, and the message should be displayed to the editor.
    • One scenario could be that the preview host is unavailable.
    • ...or perhaps some logic prerequisites are not met for the preview to work.

This also means that the client no longer has to "enter" preview by POST'ing to the /preview endpoint, so this endpoint has been deprecated. Instead, the (server-side) URL provider will be responsible for establishing a valid preview context on the target host - including the default/core URL provider.

Testing this PR using the API

Requesting the new preview-url endpoint should yield the default preview URL for the passed key.

For example: /document/54161cd8-f36b-4f97-8d43-c51118d20376/preview-url?providerAlias=umbDocumentUrlProvider&culture=en-US&segment=seg-1

...should yield:

{
  "message": null,
  "provider": "umbDocumentUrlProvider",
  "culture": "en-US",
  "url": "https://localhost:44339/umbraco/preview?id=54161cd8-f36b-4f97-8d43-c51118d20376&culture=en-US&segment=seg-1"
}

Adding a custom URL provider with a preview URL implementation should enable querying the endpoint for that provider.

For example, with this URL provider:

using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;

namespace My.Site;

public class MyUrlProvider : IUrlProvider
{
    public string Alias => "MyUrlProvider";

    public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current)
        => null;

    public IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
        => [];

    public Task<UrlInfo?> GetPreviewUrlAsync(IContent content, string? culture, string? segment)
        => Task.FromResult<UrlInfo?>(
            new UrlInfo(
                url: new Uri($"https://preview.me/{culture ?? "inv"}/{segment ?? "def"}/{content.Key}"),
                provider: Alias,
                culture: culture,
                message: $"Preview.me ({culture ?? "invariant"})",
                isExternal: true));
}

public class MyUrlProviderComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
        => builder.AddUrlProvider<MyUrlProvider>();
}

...the query: /document/54161cd8-f36b-4f97-8d43-c51118d20376/preview-url?providerAlias=MyUrlProvider&culture=en-US&segment=seg-1

...should yield

{
  "message": "Preview.me (en-US)",
  "provider": "MyUrlProvider",
  "culture": "en-US",
  "url": "https://preview.me/en-US/seg-1/54161cd8-f36b-4f97-8d43-c51118d20376"
}

Testing this PR using the client

To utilize the custom URL provider above from the backoffice client, you'll need to add include this client extension:

{
  "name": "My UrlProvider",
  "alias": "My.UrlProvider",
  "extensions": [
    {
      "type": "workspaceActionMenuItem",
      "kind": "previewOption",
      "alias": "My.Custom.PreviewOption",
      "name": "My Custom Preview Option",
      "forWorkspaceActions": "Umb.WorkspaceAction.Document.SaveAndPreview",
      "weight": 101,
      "meta": {
        "icon": "icon-umbraco",
        "label": "Preview.me",
        "urlProviderAlias": "MyUrlProvider"
      }
    }
  ]
}

Save that as umbraco-package.json in a folder under App_Plugins and restart the client. You'll see the preview option as the default, due to the weight of the extension:

image

The regular "Save and preview" option is found in the pop-up:

image

@madsrasmussen
Copy link
Contributor

Hi @kjac

It looks good from a Backoffice/client perspective. The only thing I thought about was relative vs absolute URLs. In the spirit of SPAs and how we already handle all image URLs, would it then be possible always to return an absolute URL? We want to be able to run the Backoffice on a different server than the backend/preview app, and with absolute URLs, we can avoid the prefixing dance on the client side.

@nielslyngsoe
Copy link
Member

@kjac looks good. I agree with Mads and then I just wanted to ask regarding a minor detail: could message be renamed to name?

@nielslyngsoe
Copy link
Member

@kjac another thought, what are we supposed to use isExternal for? Maybe you had a few thoughts that would be good to state :-)

@madsrasmussen
Copy link
Contributor

madsrasmussen commented Aug 29, 2025

Why isExternal

I don’t know if these are Kenn’s thoughts too, but the client needs to know when to add query params to the preview URL.

All the external URLs in the example come fully fleshed out. We could probably agree on the query params, since we already have some dependency on each other, if we like that better? 🤔

@kjac
Copy link
Contributor Author

kjac commented Sep 1, 2025

I would love to generate an absolute URL for the default preview - and we also might be able to do it... but will it work well for the client in a multi-tenant setup with multiple host names for the backoffice?

The isExternal notation is required because internal preview URLs must be handled differently from the external ones. For internal preview URLs, the client must:

  1. Initialize the user state against the Umbraco backoffice server (there's an API for that, it's used today).
  2. Append culture and segment query string variables matching the current client state.

External preview URLs should not be tampered with - the client should expect them to be complete (perhaps we'll introduce some templating options later on for inserting culture and/or segment). And of course, the external preview URLs do not require a user state on the Umbraco backoffice server 😉

@kjac
Copy link
Contributor Author

kjac commented Sep 1, 2025

Also please note that a preview URL can contain both message AND url - as well as either/or. For example:

[
  {
    "id": "f5b814c7-f435-4dda-93f3-9dd153fefa7f",
    "urlInfos": [
      {
        "message": "This page cannot be previewed because [reason]",
        "culture": null,
        "url": null,
        "isExternal": false
      },
      {
        "message": null,
        "culture": null,
        "url": "https://preview.somewhere/f5b814c7-f435-4dda-93f3-9dd153fefa7f",
        "isExternal": true
      },
      {
        "message": "Preview for brand One",
        "culture": null,
        "url": "https://preview.brand.one/f5b814c7-f435-4dda-93f3-9dd153fefa7f",
        "isExternal": true
      },
      {
        "message": "Preview for brand Two",
        "culture": null,
        "url": "https://preview.brand.two/f5b814c7-f435-4dda-93f3-9dd153fefa7f",
        "isExternal": true
      }
    ]
  }
]

@kjac
Copy link
Contributor Author

kjac commented Sep 2, 2025

I have updated this whole thing; the description reflects the new functionality - don't bother with the comments up until this point 😆

Fixed TS errors

Added temp stub for `getPreviewUrl`
reworked using the "default" `previewOption` kind.
to a more suitable filename.

Exported element so can be reused in other packages,
e.g. documents, for the new "save and preview" feature.
to work with first action's manifest/API.
Re-engineered to make a "urlProvider" kind for `workspaceActionMenuItem`.
This is to simplify the extension point and surrounding logic.
lauraneto and others added 2 commits October 7, 2025 16:30
# Conflicts:
#	src/Umbraco.Web.UI.Client/src/packages/core/backend-api/sdk.gen.ts
@kjac kjac marked this pull request as ready for review October 8, 2025 05:24
Copy link
Contributor

@AndyButland AndyButland left a comment

Choose a reason for hiding this comment

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

Code all looks good to me - just one tiny comment. Will do some testing now before approving.

While I'm thinking about it, for the documentation:

  • Please can you review what we have documented that needs updating? The 16 docs have been copied over to the 17 folder on the docs repo, so you can create a PR to update those pages.
  • Please can you also update this pending PR so we add a section about the breaking changes in this area?

@kjac
Copy link
Contributor Author

kjac commented Oct 8, 2025

As for the docs - I have a separate task for amending docs for 17 for this change.

Will update the breaking changes article 👍

Copy link
Contributor

@AndyButland AndyButland left a comment

Choose a reason for hiding this comment

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

All looks to work as expected when testing out with the default and with a custom URL provider.

Copy link
Contributor

@iOvergaard iOvergaard left a comment

Choose a reason for hiding this comment

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

I had already approved the FE part on Lee's pull request, so just adding my checkmark here as well for posterity. LGTM.

@iOvergaard iOvergaard merged commit 17a5477 into v17/dev Oct 8, 2025
26 checks passed
@iOvergaard iOvergaard deleted the v17/feature/preview-urls branch October 8, 2025 06:27
@iOvergaard iOvergaard changed the title Serverside generated preview URLs Preview mode: Server-side generated preview URLs Nov 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants