Skip to content

Commit

Permalink
V2 - Added template URl and dynamic placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
jesperweber committed Mar 28, 2023
1 parent 76d6056 commit 2a02126
Show file tree
Hide file tree
Showing 22 changed files with 201 additions and 190 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ jobs:
- name: Build
run: dotnet build $PROJECT --configuration $BUILD_CONFIG -p:Version=$BUILD_VERSION --no-restore

#- name: Run tests
# run: dotnet test /p:Configuration=$env:BUILD_CONFIG --no-restore --no-build --verbosity normal
- name: Run tests
run: dotnet test /p:Configuration=$env:BUILD_CONFIG --no-restore --no-build --verbosity normal

- name: Publish on nuget.org
if: startsWith(github.ref, 'refs/heads/release')
Expand Down
6 changes: 6 additions & 0 deletions Our.Umbraco.HeadlessPreview.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documents", "Documents", "{
umbraco-marketplace.json = umbraco-marketplace.json
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Our.Umbraco.HeadlessPreview.Tests", "src\Our.Umbraco.HeadlessPreview.Tests\Our.Umbraco.HeadlessPreview.Tests.csproj", "{A5161DF7-36B2-4550-A100-51C7BA2F77CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -27,6 +29,10 @@ Global
{B99C534A-9DBE-4A80-8E70-0C9227F3BC44}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B99C534A-9DBE-4A80-8E70-0C9227F3BC44}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B99C534A-9DBE-4A80-8E70-0C9227F3BC44}.Release|Any CPU.Build.0 = Release|Any CPU
{A5161DF7-36B2-4550-A100-51C7BA2F77CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A5161DF7-36B2-4550-A100-51C7BA2F77CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5161DF7-36B2-4550-A100-51C7BA2F77CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5161DF7-36B2-4550-A100-51C7BA2F77CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,35 @@ Install-Package Our.Umbraco.HeadlessPreview -Version <version>
The package can be configured using the `appsetings.json` file or using the UI which will save the configuration in the database.


| Setting | Default value | Description |
|---------- |------------- |------ |
| `UseUmbracoHostnames` | `false` | If set to true the domain from the `Culture and Hostnames` for the site is used as preview hostname.<br/><br/>If set to false the value from `StaticHostname` is used. |
| `StaticHostname` | `''` | The hostname used for preview if `UseUmbracoHostnames` is set to false. |
| `RelativePath` | `'api/preview'` | The relative path to be used after the hostname. |
| `Secret` | `''` | A secret value passed to the preview site for authentication. |
| Setting | Default value | Description |
|---------- |------------- |------ |
| `TemplateUrl` | `` | The URL used for preview. It can contain dynamic placeholder values to support different types of URL's.<br /><br />Typically used template URL are:<br/><br/><ul><li>https://mysite.com/api/preview?slug=\{slug\}&secret=mySecret</li><li>\{hostname\}/api/preview?slug=\{slug\}&secret=mySecret</li><li>https://mysite.com/\{slug\}?preview=true</li></ul> |

### UI

If you just have a single environment it's easy to just configure the plugin directly from the Umbraco Backoffice in the Settings section.

![Configuration](https://raw.githubusercontent.com/jesperweber/Our.Umbraco.HeadlessPreview/main/screenshots/Settings.png "Headless Preview Settings")
![Configuration](https://raw.githubusercontent.com/jesperweber/Our.Umbraco.HeadlessPreview/main/screenshots/SettingsV2.png "Headless Preview Settings")

### appsettings.json
This is typically the preferred way if you have a multi environment setup as you can use environment specific settings.

``` json
"HeadlessPreview": {
"UseUmbracoHostnames": false,
"StaticHostname": "https://mysite.com",
"RelativePath": "api/preview",
"Secret": "mySecret"
"TemplateUrl": "https://mysite.com/api/preview?slug={slug}&secret=mySecret"
}
```

## Placeholders

Placeholders are predefined keys enclosed in curly braces that you can use in your tempalte URL. Placeholders are automatically replaced with real values based on the page you are previewing.


| Placeholder | Description |
|---------- |------ |
| `{hostname}` | The hostname added on nearest ancestor node or self with the right culture in Umbraco. If multiple hostname has same culture it takes the first. |
| `{slug}` | The relative path of the page being previewed. |

## Changelog

See new features, fixes and breaking changes for each [Release](https://github.com/jesperweber/Our.Umbraco.HeadlessPreview/releases).
Expand Down
5 changes: 5 additions & 0 deletions releaseNotes/2.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
V2 uses a template URL in combination with placeholders to give you full flexibility to build exactly the preview URL you want.

## Added
- New template URL concept with dynamic placeholder values
- Hostname is selected based on the culture you are previewing
Binary file added screenshots/SettingsV2.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit.Analyzers" Version="3.5.0" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Our.Umbraco.HeadlessPreview\Our.Umbraco.HeadlessPreview.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using NUnit.Framework;
using Our.Umbraco.HeadlessPreview.Models;
using Our.Umbraco.HeadlessPreview.Services;

namespace Our.Umbraco.HeadlessPreview.Tests.Services;

public class TemplateUrlParserTest
{
[Test]
[TestCase("https://test.local/api/preview?slug={slug}", ExpectedResult = new[] { TemplateUrlPlaceHolder.Slug })]
[TestCase("https://test.local/api/preview?slug={SLUG}", ExpectedResult = new[] { TemplateUrlPlaceHolder.Slug })]
[TestCase("{hostname}/api/preview", ExpectedResult = new[] { TemplateUrlPlaceHolder.Hostname })]
[TestCase("{HOSTNAME}/api/preview", ExpectedResult = new[] { TemplateUrlPlaceHolder.Hostname })]
[TestCase("{hostname}/api/preview?slug={slug}", ExpectedResult = new[] { TemplateUrlPlaceHolder.Hostname, TemplateUrlPlaceHolder.Slug })]
[TestCase("{HOSTNAME}/api/preview?slug={SLUG}", ExpectedResult = new [] { TemplateUrlPlaceHolder.Hostname, TemplateUrlPlaceHolder.Slug })]
public TemplateUrlPlaceHolder[] GetPlaceHolders_Returns_Placeholders(string templateUrl)
{
var subject = new TemplateUrlParser();

var result = subject.GetPlaceHolders(templateUrl);

return result;
}

[Test]
[TestCase("https://test.local/api/preview?slug={slug}", null, "/testpage", ExpectedResult = "https://test.local/api/preview?slug=/testpage")]
[TestCase("https://test.local/api/preview?slug={SLUG}", null, "/testpage", ExpectedResult = "https://test.local/api/preview?slug=/testpage")]
[TestCase("{hostname}/api/preview", "https://test.local", null, ExpectedResult = "https://test.local/api/preview")]
[TestCase("{HOSTNAME}/api/preview", "https://test.local", null, ExpectedResult = "https://test.local/api/preview")]
[TestCase("{hostname}/api/preview?slug={slug}", "https://test.local", "/testpage", ExpectedResult = "https://test.local/api/preview?slug=/testpage")]
[TestCase("{HOSTNAME}/api/preview?slug={SLUG}", "https://test.local", "/testpage", ExpectedResult = "https://test.local/api/preview?slug=/testpage")]
public string Parse_Returns_Parsed_Url(string templateUrl, string hostname, string slug)
{
var subject = new TemplateUrlParser();

var result = subject.Parse(templateUrl, hostname, slug);

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,22 @@
vm.getConfiguration();
}

vm.toggleUseUmbracoHostnames = function () {
vm.configuration.useUmbracoHostnames = !vm.configuration.useUmbracoHostnames;
vm.updateTemplateUrl();
}

vm.updateTemplateUrl = function () {

var hostname = vm.configuration.useUmbracoHostnames ? "[siteHostname]" : vm.configuration.staticHostname?.trimEnd('/');
var relativePath = vm.configuration.relativePath?.trimStart('/');

var parametersToAdd = "?slug=[relativePathOfPage]";
if (vm.configuration.secret)
parametersToAdd += "&secret=" + vm.configuration.secret;

vm.configuration.templateUrl = hostname + "/" + relativePath + parametersToAdd
}

vm.getConfiguration = function () {
vm.loadingConfiguration = true;
vm.buttonState = "busy";
headlessPreviewDashboardResources.getConfiguration()
.then(function (result) {
if (result.data.isSuccess) {
vm.configuration.useUmbracoHostnames = result.data.data.useUmbracoHostnames;
vm.configuration.staticHostname = result.data.data.staticHostname;
vm.configuration.relativePath = result.data.data.relativePath;
vm.configuration.secret = result.data.data.secret;
vm.configuration.templateUrl = result.data.data.templateUrl;
vm.configuration.configuredFromSettingsFile = result.data.data.configuredFromSettingsFile;
vm.loadingConfiguration = false;
vm.buttonState = null;

vm.updateTemplateUrl();
}
});
};

vm.saveConfiguration = function () {
vm.buttonState = "busy";
if (!vm.configuration.useUmbracoHostnames && !vm.configuration.staticHostname) {
notificationsService.error("You must add a static hostname if you are not using Umbraco hostnames");
vm.buttonState = null;
return;
}

headlessPreviewDashboardResources.saveConfiguration(vm.configuration)
.then(function (result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

return {
changePreviewButton: function (data) {

var openPreview = (page) => {
window.open("/umbraco/backoffice/headlesspreview?id=" + page.id);
window.open("/umbraco/backoffice/headlesspreview?id=" + page.id + "&culture=" + tryGetCulture(page));
}

var tryGetCulture = (page) => {
var culture = page?.variants.find(variant => variant.active)?.language?.culture;

return culture ?? '';
}

var interval = setInterval(function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@
#headlessPreviewDashboard .configuration-dashboard-property input {
width: 750px;
}

#headlessPreviewDashboard .configuration-dashboard-property select {
width: 750px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,58 +13,33 @@
</div>

<div class="configuration-dashboard-property">
<label for="use-umbraco-hostnames">Use Umbraco Hostnames</label><br />

<button role="checkbox" type="button" class="umb-toggle"
ng-disabled="vm.buttonState === 'busy' || vm.configuration.configuredFromSettingsFile"
ng-class="{'umb-toggle--checked': vm.configuration.useUmbracoHostnames, 'umb-toggle--disabled': vm.buttonState === 'busy' || vm.configuration.configuredFromSettingsFile}"
ng-click="vm.toggleUseUmbracoHostnames()">
<div class="umb-toggle__toggle">
<span aria-hidden="true" class="umb-toggle__icon umb-toggle__icon--left umb-icon" icon="icon-check">
<span class="umb-icon__inner icon-check">
<span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M461.884 68.14c-132.601 81.297-228.817 183.87-272.048 235.345l-105.874-82.95-46.751 37.691 182.941 186.049c31.485-80.646 131.198-238.264 252.956-350.252L461.884 68.14z"></path>
</svg>
</span>
</span>
</span>
<span aria-hidden="true" class="umb-toggle__icon umb-toggle__icon--right umb-icon" icon="icon-wrong">
<span class="umb-icon__inner icon-wrong">
<span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M422.952 371.305L307.064 255.418l115.884-115.887-51.722-51.723L255.34 203.693 139.457 87.812l-51.726 51.719 115.885 115.885L87.731 371.305l51.726 51.721L255.344 307.14l115.884 115.882z"></path>
</svg>
</span>
</span>
</span>
<div class="umb-toggle__handler"></div>
</div>
</button>
</div>
<div class="configuration-dashboard-property" ng-if="!vm.configuration.useUmbracoHostnames">
<label for="static-hostname">Static Hostname</label><br />
<input type="text" id="static-hostname" class="input-field" ng-change="vm.updateTemplateUrl()" ng-model="vm.configuration.staticHostname" ng-disabled="vm.buttonState === 'busy' || vm.configuration.configuredFromSettingsFile" />
</div>
<div class="configuration-dashboard-property">
<label for="relative-path">Relative Path</label><br />
<input type="text" id="relative-path" class="input-field" ng-change="vm.updateTemplateUrl()" ng-model="vm.configuration.relativePath" ng-disabled="vm.buttonState === 'busy' || vm.configuration.configuredFromSettingsFile" />
<strong>Placeholders</strong>
<ul>
<li><span>{hostname}</span> - The hostname added on nearest ancestor node or self with the right culture in Umbraco. If multiple hostname has same culture it takes the first.</li>
<li><span>{slug}</span> - The relative path of the page being previewed.</li>
</ul>
</div>

<div class="configuration-dashboard-property">
<label for="secret">Secret</label><br />
<input type="text" id="secret" class="input-field" ng-change="vm.updateTemplateUrl()" ng-model="vm.configuration.secret" ng-disabled="vm.buttonState === 'busy' || vm.configuration.configuredFromSettingsFile" />
<strong>Template Url Examples</strong>
<ul>
<li>https://mysite.com/api/preview?slug={slug}&secret=mySecret</li>
<li>{hostname}/api/preview?slug={slug}&secret=mySecret</li>
<li>https://mysite.com/{slug}?preview=true</li>
</ul>
</div>

<div class="configuration-dashboard-property">
<label>Configured Preview URL</label><br />
<span>{{vm.configuration.templateUrl}}</span>
<label for="template-url">Template URL</label><br />
<input type="text" id="template-url" class="input-field" placeholder="Example: https://mysite.com/api/preview?slug={slug}&secret=mySecret" ng-model="vm.configuration.templateUrl" ng-disabled="vm.buttonState === 'busy' || vm.configuration.configuredFromSettingsFile" />
</div>

<umb-button ng-if="!vm.configuration.configuredFromSettingsFile" action="vm.saveConfiguration()"
type="button"
button-style="success"
state="vm.configState"
label="Save configuration"
disabled="vm.buttonState === 'busy' || vm.configuration.configuredFromSettingsFile || (!vm.configuration.useUmbracoHostnames && !vm.configuration.staticHostname)">
disabled="vm.buttonState === 'busy' || vm.configuration.configuredFromSettingsFile || (!vm.configuration.templateUrl)">
</umb-button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public void Compose(IUmbracoBuilder builder)
});

builder.Services.AddSingleton<IPreviewConfigurationService, PreviewConfigurationService>();
builder.Services.AddSingleton<ITemplateUrlParser, TemplateUrlParser>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,6 @@ public ApiResponse<PreviewConfiguration> GetConfiguration()
[HttpPost]
public IActionResult SaveConfiguration(PreviewConfiguration configuration)
{
if (!configuration.UseUmbracoHostnames && string.IsNullOrEmpty(configuration.StaticHostname))
{
return BadRequest(
new Response
{
StatusCode = HttpStatusCode.BadRequest,
Success = false,
Message = "Configuration is configured to not not use Umbraco hostnames but no static hostname has been set."
});
}

try
{
_previewConfigurationService.Save(configuration);
Expand Down

0 comments on commit 2a02126

Please sign in to comment.