Open Alerinos opened 1 year ago
@Alerinos thanks for contacting us.
Then, when compiling the code, razor will detect whether there is already
<meta name="theme-color">
in<HeadContent>
, if so, it will replace its value, if not, it will do nothing.
What is preventing you from rendering your own component inside the HeadContent
component where you keep the common code you want and append additional code from other components?
Seems that what you are looking for is a higher level concept that allows you to decide when to add additional stuff or override it from the page. The HeadContent and HeadOutlet components are not responsible for that, they only care about putting the content in the right place.
There are not good heuristics Blazor can follow either at compile or runtime, these things are an app decision, so we think it is better handled by the developer on their own component.
@javiercn
I understand the assumption of the components and because of their universality it is difficult to find a good solution here.
I think you could enter a default value.
We can put there, for example, a page description and other basic meta. If a subpage stands out, we replace <HeadComponent>
with these values.
Looking at the component code, I know that we can create a clone ourselves, but shouldn't there also be a default one for the bottom of the page?
Let's imagine that there is only one subpage on which there is an editor, it is in JS. I don't want to load it for the whole page for performance reasons, putting the JS code in HeadComponent
will delay rendering. Using JSInvoke is possible but not convenient:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "/lib/script.js");
await module.InvokeVoidAsync(...);
}
await base.OnAfterRenderAsync(firstRender);
}
What do you think of a similar default component for JS scripts?
edit: We are not able to manipulate <meta ..>
elements like <PageTitle>
?
We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.
This would be useful, but we found a workaround that works for us until there is some built-in support.
We were able to achieve different title, description, image, etc. for specific pages by updating the <head>
during server rendering. This is required for per-page SEO and link previews.
DefaultMetadata
component, which renders <HeadContent>
with some default page metadata elements. The component takes an optional parameter to control the metadata elements.DefaultMetadata
component to <head>
in _Host.cshtml
HeadOutlet
component to <head>
in _Host.cshtml
, to emit content from <HeadContent>
(default in newer Blazor templates)DefaultMetadata
component on pages that want to override the page metadata and pass in values by parameter to add or override the defaultsThe key insight is that HeadOutlet
only emits the last rendered HeadContent
, so a page may override the default HeadContent
rendered by _Host.cshtml
.
Relevant docs: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/control-head-content?view=aspnetcore-8.0
_Host.cshtml
<head>
@* Custom component to add default page metadata, with parameter to optionally override per-page. *@
<component type="typeof(DefaultMetadata)" render-mode="ServerPrerendered" />
@* Allow pages to emit custom content in <head> via <PageTitle> and <HeadContent> components. *@
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
MyPage.razor
@page "/my-page"
<DefaultMetadata Options="@_metadataOptions" />
MyPage.razor.cs
public partial class MyPage
{
/// <inheritdoc />
public override async Task SetParametersAsync(ParameterView parameters)
{
await base.SetParametersAsync(parameters);
// Initialize metadata on server-side render.
await UpdatePageMetadataAsync();
}
private async Task UpdatePageMetadataAsync()
{
// Get some data asynchronously, will block first render of DOM server-side before passed to browser and hand-off to client side render.
var someData = await GetSomeDataAsync();
// Set data, bound to DefaultMetadata component in page.
_metadataOptions = new DefaultMetadataOptions(
Title: "My page custom title",
Description: "My page custom description",
Image: null, // Set URL to image for link previews to page.
ImageAlt: null, // Set alt text for Image.
ExtraMetaElements: GetExtraMetaElements(someData));
// Trigger re-render of page and DefaultMetadata component to pick up the new values. Still on first render, server-side.
await InvokeAsync(StateHasChanged);
}
private static MetaElement[] GetExtraMetaElements(SomeData someData)
{
// Add custom metadata for this page or update values in the default metadata.
return new[]
{
MetaElement.ByProp("al:ios:url", $"myapp://user/{someData.UserId}"),
MetaElement.ByProp("al:android:url", $"myapp://user/{someData.UserId}"),
};
}
}
DefaultMetadata.razor
Note: The dependency on Toolbelt.Blazor.HeadElement is not essential, you can easily write your own MetaElement
type to pass data to the component.
MetaElement source: https://github.com/jsakamoto/Toolbelt.Blazor.HeadElement/blob/master/Abstractions/MetaElement.cs
@using Serilog
@using Toolbelt.Blazor.HeadElement
@* Default metadata to put in HTML head. Add or override metadata with <see cref="Options"/>. Requires <HeadOutlet> in _Host.cshtml. *@
<HeadContent>
@{
MetaElement[] metaElements = GetMetaElements();
}
@foreach (MetaElement element in metaElements)
{
@if (element.Name is { Length: > 0 } name)
{
<meta name="@name" content="@element.Content">
}
else if (element.Property is { Length: > 0 } property)
{
<meta property="@property" content="@element.Content">
}
else if (element.HttpEquiv is { Length: > 0 } httpEquiv)
{
<meta http-equiv="@httpEquiv" content="@element.Content">
}
}
</HeadContent>
DefaultMetadata.razor.cs
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Toolbelt.Blazor.HeadElement;
namespace MyApp.Pages;
/// <summary>
/// Page metadata options. Add or override the default meta elements in <see cref="DefaultMetadata"/> with this class.
/// </summary>
/// <param name="Title">Page title in metadata, affects preview of links.</param>
/// <param name="Description">Page description in metadata, affects preview of links.</param>
/// <param name="Image">Page image in metadata, affects preview of links.</param>
/// <param name="ImageAlt">Page image alternative text in metadata, affects preview of links.ImageAlt"/>.</param>
/// <param name="ExtraMetaElements">Extra meta elements to add or to override the default meta elements in <see cref="DefaultMetadata"/>.</param>
public record DefaultMetadataOptions(
string? Title = null,
string? Description = null,
string? Image = null,
string? ImageAlt = null,
IList<MetaElement>? ExtraMetaElements = null);
/// <summary>
/// Default metadata to put in HTML head.<br />
/// <br />
/// Add or override metadata with <see cref="Options"/>.<br />
/// Requires <see cref="HeadOutlet"/> in _Host.cshtml.
/// </summary>
public partial class DefaultMetadata
{
/// <summary>
/// Options for adding custom page metadata or overriding the default metadata, such as page title, description and image used in link previews.
/// </summary>
[Parameter]
public DefaultMetadataOptions? Options { get; set; }
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
protected override void OnParametersSet()
{
base.OnParametersSet();
Url = NavigationManager.Uri;
}
private string? Url { get; set; }
private MetaElement? GetMetaName(string key, string? defaultValue)
{
string? value = Options?.ExtraMetaElements?.FirstOrDefault(m => m.Name == key)?.Content ?? defaultValue;
return value is null ? null : MetaElement.ByName(key, value);
}
private MetaElement? GetMetaProp(string key, string? defaultValue)
{
string? value = Options?.ExtraMetaElements?.FirstOrDefault(m => m.Property == key)?.Content ?? defaultValue;
return value is null ? null : MetaElement.ByProp(key, value);
}
/// <summary>Gets the default meta elements, optionally overridden by injected by parameters.</summary>
private MetaElement[] GetMetaElements()
{
var title = Options?.Title ?? "My default page title";
var image = Options?.Image ?? "https://myapp.com/images/logo.png";
var imageAlt = Options?.ImageAlt ?? "My logo description";
var description = Options?.Description ?? "My default page description";
const string siteName = "My app";
string? url = Url;
return new[]
{
GetMetaName("title", title),
GetMetaName("description", description),
GetMetaName("keywords", "my, app"),
GetMetaName("language", "English"),
// OpenGraph
GetMetaProp("og:type", "website"),
GetMetaProp("og:site_name", siteName),
GetMetaProp("og:title", title),
GetMetaProp("og:description", description),
GetMetaProp("og:image", image),
GetMetaProp("og:image:alt", imageAlt),
GetMetaProp("og:url", url),
// GetMetaProperty("og:image:width", "250"),
// GetMetaProperty("og:image:height", "250"),
// Twitter
// GetMetaProp("twitter:site", ""), // Add twitter account
GetMetaProp("twitter:card", "summary_large_image"),
GetMetaProp("twitter:url", url),
GetMetaProp("twitter:title", title),
GetMetaProp("twitter:description", description),
GetMetaProp("twitter:image", image),
GetMetaProp("twitter:image:alt", imageAlt),
GetMetaProp("twitter:app:id:iphone", "0000000000"),
GetMetaProp("twitter:app:name:iphone", "MyApp"),
GetMetaProp("twitter:app:id:googleplay", "com.my.app"),
GetMetaProp("twitter:app:name:googleplay", "MyApp"),
// Facebook / Meta
GetMetaProp("fb:app_id", "000000000000000"),
GetMetaProp("al:ios:url", "myapp://"),
GetMetaProp("al:ios:app_store_id", "0000000000"),
GetMetaProp("al:ios:app_name", "MyApp"),
GetMetaProp("al:android:url", "myapp://"),
GetMetaProp("al:android:package", "com.my.app"),
GetMetaProp("al:android:app_name", "MyApp"),
// Apple
GetMetaProp("apple-itunes-app", $"app-id=0000000000, app-argument={url}"),
}
.Where(e => e?.Content.Length > 0)
.Select(e => e!)
.ToArray();
}
}
We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.
Is there an existing issue for this?
Is your feature request related to a problem? Please describe the problem.
Currently, we have
<HeadContent>
for head tags.It lacks default values, let's assume we have a tag like this:
<meta name="theme-color" content="#ff6100">
We can put it in _Host.cshtml which will be available on every subpage. What if a subpage wants to modify it? There are currently two ways: 1.) Duplication of the value by HeadContent + _Host.cshtml. bad for SEO 2.) Every subpage should be placed in HeadContent, good for seo but bad for programmer. Code duplication and lots of problemsAlternatively, we can create an intermediate code, unfortunately also bad for the programmer. You will need to remember about the implementation on each page, example:
I propose to create a default value during definition. That is:
Then, when compiling the code, razor will detect whether there is already
<meta name="theme-color">
in<HeadContent>
, if so, it will replace its value, if not, it will do nothing.Describe the solution you'd like
Let me know what you think about it, I think it's worth discussing this idea and expanding on such a basic thing that has been omitted.
Additional context
No response