diff --git a/Oqtane.Client/App.razor b/Oqtane.Client/App.razor index 8a91c1c8..701faf81 100644 --- a/Oqtane.Client/App.razor +++ b/Oqtane.Client/App.razor @@ -1,6 +1,8 @@ +@using Microsoft.AspNetCore.Http @inject IInstallationService InstallationService @inject IJSRuntime JSRuntime @inject SiteState SiteState +@inject IServiceProvider ServiceProvider @if (_initialized) { @@ -30,35 +32,47 @@ } @code { - [Parameter] - public string AntiForgeryToken { get; set; } + [Parameter] + public string AntiForgeryToken { get; set; } - [Parameter] - public string Runtime { get; set; } + [Parameter] + public string Runtime { get; set; } - [Parameter] - public string RenderMode { get; set; } + [Parameter] + public string RenderMode { get; set; } - [Parameter] - public int VisitorId { get; set; } + [Parameter] + public int VisitorId { get; set; } - [Parameter] - public string RemoteIPAddress { get; set; } + [Parameter] + public string RemoteIPAddress { get; set; } - [Parameter] - public string AuthorizationToken { get; set; } + [Parameter] + public string AuthorizationToken { get; set; } - private bool _initialized = false; - private string _display = "display: none;"; - private Installation _installation = new Installation { Success = false, Message = "" }; + private bool _initialized = false; + private string _display = "display: none;"; + private Installation _installation = new Installation { Success = false, Message = "" }; - private PageState PageState { get; set; } + private PageState PageState { get; set; } - protected override async Task OnParametersSetAsync() - { - SiteState.RemoteIPAddress = RemoteIPAddress; - SiteState.AntiForgeryToken = AntiForgeryToken; - SiteState.AuthorizationToken = AuthorizationToken; + private IHttpContextAccessor accessor; + + protected override async Task OnParametersSetAsync() + { + SiteState.RemoteIPAddress = RemoteIPAddress; + SiteState.AntiForgeryToken = AntiForgeryToken; + SiteState.AuthorizationToken = AuthorizationToken; + + accessor = (IHttpContextAccessor)ServiceProvider.GetService(typeof(IHttpContextAccessor)); + if (accessor != null) + { + SiteState.IsPrerendering = !accessor.HttpContext.Response.HasStarted; + } + else + { + SiteState.IsPrerendering = true; + } _installation = await InstallationService.IsInstalled(); if (_installation.Alias != null) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 75a1d6c8..6e3fc7cc 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -278,6 +278,7 @@ namespace Oqtane.Modules SiteState.Properties.PageTitle = title; } + // note - only supports links and meta tags - not scripts public void AddHeadContent(string content) { if (string.IsNullOrEmpty(SiteState.Properties.HeadContent)) diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index 49017206..f4f02565 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -6,9 +6,9 @@ @DynamicComponent @code { - [CascadingParameter] PageState PageState { get; set; } + [CascadingParameter] PageState PageState { get; set; } - RenderFragment DynamicComponent { get; set; } + RenderFragment DynamicComponent { get; set; } protected override void OnParametersSet() { @@ -38,21 +38,21 @@ favicon = Utilities.FileUrl(PageState.Alias, PageState.Site.FaviconFileId.Value); } headcontent += $"\n"; - // head content - if (!string.IsNullOrEmpty(PageState.Site.HeadContent)) - { - headcontent += ProcessHeadContent(PageState.Site.HeadContent, "site") + "\n"; - } - if (!string.IsNullOrEmpty(PageState.Page.HeadContent)) - { - headcontent += ProcessHeadContent(PageState.Page.HeadContent, $"page{PageState.Page.PageId}") + "\n"; - } // stylesheets foreach (Resource resource in PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Stylesheet)) { var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + resource.Url; headcontent += CreateLink(url, resource.Integrity, resource.CrossOrigin) + "\n"; } + // head content + if (!string.IsNullOrEmpty(PageState.Site.HeadContent)) + { + headcontent += PageState.Site.HeadContent + "\n"; + } + if (!string.IsNullOrEmpty(PageState.Page.HeadContent)) + { + headcontent += PageState.Page.HeadContent + "\n"; + } SiteState.Properties.HeadContent = headcontent; DynamicComponent = builder => @@ -65,87 +65,75 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("= 0) + if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("", index) + 9 - index); - var attributes = script.Substring(0, script.IndexOf(">")).Replace("\"", "").Split(" "); - string id = ""; - string src = ""; - string integrity = ""; - string crossorigin = ""; - string type = ""; - foreach (var attribute in attributes) + // inject scripts into page dynamically + var interop = new Interop(JSRuntime); + var scripts = new List(); + var count = 0; + var index = PageState.Page.HeadContent.IndexOf("= 0) { - if (attribute.Contains("=")) + var script = PageState.Page.HeadContent.Substring(index, PageState.Page.HeadContent.IndexOf("", index) + 9 - index); + // get script attributes + var attributes = script.Substring(0, script.IndexOf(">")).Replace("\"", "").Split(" "); + string id = ""; + string src = ""; + string integrity = ""; + string crossorigin = ""; + string type = ""; + foreach (var attribute in attributes) { - var value = attribute.Split("="); - switch (value[0]) + if (attribute.Contains("=")) { - case "id": - id = value[1]; - break; - case "src": - src = value[1]; - break; - case "integrity": - integrity = value[1]; - break; - case "crossorigin": - crossorigin = value[1]; - break; - case "type": - type = value[1]; - break; + var value = attribute.Split("="); + switch (value[0]) + { + case "id": + id = value[1]; + break; + case "src": + src = value[1]; + break; + case "integrity": + integrity = value[1]; + break; + case "crossorigin": + crossorigin = value[1]; + break; + case "type": + type = value[1]; + break; + } } } - } - if (!string.IsNullOrEmpty(src)) - { - src = (src.Contains("://")) ? src : PageState.Alias.BaseUrl + src; - await interop.IncludeScript(id, src, integrity, crossorigin, type, "", "head"); - } - else - { - if (id == "") + // inject script + if (!string.IsNullOrEmpty(src)) { - count += 1; - id = $"page{PageState.Page.PageId}-script{count}"; + src = (src.Contains("://")) ? src : PageState.Alias.BaseUrl + src; + scripts.Add(new { href = src, bundle = "", integrity = integrity, crossorigin = crossorigin, es6module = (type == "module"), location = "head" }); } - index = script.IndexOf(">") + 1; - await interop.IncludeScript(id, "", "", "", "", script.Substring(index, script.IndexOf("") - index), "head"); + else + { + // inline script must have an id attribute + if (id == "") + { + count += 1; + id = $"page{PageState.Page.PageId}-script{count}"; + } + index = script.IndexOf(">") + 1; + await interop.IncludeScript(id, "", "", "", "", script.Substring(index, script.IndexOf("") - index), "head"); + } + index = PageState.Page.HeadContent.IndexOf("= 0) - { - var script = headcontent.Substring(index, headcontent.IndexOf("", index) + 9 - index); - if (!script.Contains("src=") && !script.Contains("id=")) + if (scripts.Any()) { - count += 1; - id += $"-script{count}"; - headcontent = headcontent.Replace(script, script.Replace(" } - @Html.Raw(Model.HeadResources) + @Html.Raw(Model.HeadResources) @if (string.IsNullOrEmpty(Model.Message)) diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index 328794ab..63d9082d 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -19,7 +19,6 @@ using Microsoft.Extensions.Primitives; using Oqtane.Enums; using Oqtane.Security; using Oqtane.Extensions; -using Oqtane.Themes; namespace Oqtane.Pages { @@ -130,10 +129,8 @@ namespace Oqtane.Pages { PWAScript = CreatePWAScript(alias, site, route); } - if (!string.IsNullOrEmpty(site.HeadContent)) - { - ProcessHeadContent(site.HeadContent, "site"); - } + // site level scripts + HeadResources += ParseScripts(site.HeadContent); // get jwt token for downstream APIs if (User.Identity.IsAuthenticated) @@ -174,8 +171,6 @@ namespace Oqtane.Pages } } - ProcessHeadContent(page.HeadContent, $"page{page.PageId}"); - // include global resources var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); foreach (Assembly assembly in assemblies) @@ -427,27 +422,20 @@ namespace Oqtane.Pages } } - private void ProcessHeadContent(string headcontent, string id) + private string ParseScripts(string headcontent) { - // add scripts to page + // iterate scripts + var scripts = ""; if (!string.IsNullOrEmpty(headcontent)) { - var count = 0; var index = headcontent.IndexOf("= 0) { - var script = headcontent.Substring(index, headcontent.IndexOf("", index) + 9 - index); - if (!script.Contains("src=") && !script.Contains("id=")) - { - count += 1; - id += $"-script{count}"; - script = script.Replace("", index) + 9 - index); index = headcontent.IndexOf(" { - console.log(src + ' loaded'); + if (src !== "") { + console.log(src + ' loaded'); + } + else { + console.log(id + ' loaded'); + } }) .catch(() => { - console.error(src + ' failed'); + if (src !== "") { + console.error(src + ' failed'); + } + else { + console.error(id + ' failed'); + } }); } - else { - if (script.id !== id) { - script.setAttribute('id', id); - } - if (type !== "") { - if (script.type !== type) { - script.setAttribute('type', type); - } - } else { - script.removeAttribute('type'); - } - if (src !== "") { - if (script.src !== this.getAbsoluteUrl(src)) { - script.removeAttribute('integrity'); - script.removeAttribute('crossorigin'); - script.src = src; - } - if (integrity !== "") { - if (script.integrity !== integrity) { - script.setAttribute('integrity', integrity); - } - } else { - script.removeAttribute('integrity'); - } - if (crossorigin !== "") { - if (script.crossOrigin !== crossorigin) { - script.setAttribute('crossorigin', crossorigin); - } - } else { - script.removeAttribute('crossorigin'); - } - } - else { - if (script.innerHTML !== content) { - script.innerHTML = content; - } - } - } }, addScript: function (script, location) { if (location === 'head') { @@ -234,6 +210,10 @@ Oqtane.Interop = { if (path === scripts[s].href && scripts[s].es6module === true) { element.type = "module"; } + if (path === scripts[s].href && scripts[s].location === 'body') { + document.body.appendChild(element); + return false; // return false to bypass default DOM insertion mechanism + } } } }) diff --git a/Oqtane.Shared/Shared/SiteState.cs b/Oqtane.Shared/Shared/SiteState.cs index 7dcea082..f3727567 100644 --- a/Oqtane.Shared/Shared/SiteState.cs +++ b/Oqtane.Shared/Shared/SiteState.cs @@ -9,7 +9,7 @@ namespace Oqtane.Shared public string AntiForgeryToken { get; set; } // passed from server for use in service calls on client public string AuthorizationToken { get; set; } // passed from server for use in service calls on client public string RemoteIPAddress { get; set; } // passed from server as cannot be reliably retrieved on client - + public bool IsPrerendering{ get; set; } private dynamic _properties; public dynamic Properties => _properties ?? (_properties = new PropertyDictionary());