使用 Blazor 设置 html 元标记
Setting html meta tags with Blazor
这是参考 #10450。
目标: 使用来自页面本身的数据为 SEO 和 Open Graph 目的设置元标记(标题、描述等)。使用 Javascript 互操作将无济于事,因为无法抓取页面。
我使用了@honkmother 的 suggestion 并将基本组件进一步向上移动以封装 <html>
标记,但由于某种原因,这影响了路由。所有链接都以 ~/
开头,我似乎不明白为什么。
我已经创建了一个示例 repo here 如果有人有兴趣看一看。
_Hosts.cshtml
@page "/"
@namespace BlazorMetaTags.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
<!DOCTYPE html>
<component type="typeof(AppBase)" render-mode="ServerPrerendered" />
AppBase.cs
using BlazorMetaTags.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace BlazorMetaTags
{
public class AppBase : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "html");
builder.AddAttribute(1, "lang", "en");
builder.OpenElement(2, "head");
builder.OpenComponent<Head>(3);
builder.CloseComponent();
builder.CloseElement();
builder.OpenElement(3, "body");
builder.OpenElement(4, "app");
builder.OpenComponent<App>(5);
builder.CloseComponent();
builder.CloseElement();
builder.OpenComponent<Body>(6);
builder.CloseComponent();
builder.AddMarkupContent(7, " <script src='_framework/blazor.server.js'></script>");
builder.CloseElement();
builder.CloseElement();
}
}
public class MetaTags
{
public string Title { get; set; } = "";
public string Description { get; set; } = "";
}
}
Head.razor 组件设置元标签
@inject AppState _appState
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@_appState.MetaTags.Title</title>
<meta name="description" content="@_appState.MetaTags.Description">
<base href="~/" />
<link rel="stylesheet" href="/css/bootstrap/bootstrap.min.css" />
<link href="/css/site.css" rel="stylesheet" />
</head>
@code {
protected override async Task OnInitializedAsync()
{
_appState.OnChange += StateHasChanged;
}
}
Body.razor
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss"></a>
</div>
AppState.cs
using System;
namespace BlazorMetaTags
{
public class AppState
{
public MetaTags MetaTags { get; private set; } = new MetaTags();
public event Action OnChange;
public void SetMetaTags(MetaTags metatags)
{
MetaTags = metatags;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddScoped<AppState>();
services.AddSingleton<WeatherForecastService>();
}
根据的建议,正确的解决方案是更改
<base href="~/" />
至
<base href="/" />
这对我有用!
我提出我对这个问题的决定。这个解决方案有几个优点:
- SEO 数据将始终是最新的,无需使用 Blazor Server 的“_framework/blazor.server.js”,这将使您能够获取各种机器人程序(包括 Postman)的最新 SEO 数据、curl 和其他非浏览器程序。
- 无需使用 DevExpress Free Blazor Utilities 和 Dev Tools 或其他类似的外部库。
- 不需要重载整个 head 标签。因为当你重载整个 head 标签并使用外部 CSS 样式时,会发生以下情况:在第一次渲染期间,CSS 样式不起作用,但开始在第二次渲染后工作,导致页面闪烁,因为 CSS 样式在第二次渲染后开始工作。
这是我经过测试的有效解决方案:
- 文件“_Host.cshtml”:
@page "/"
@namespace App.Pages
@using App.Components
@using App.Helpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
string path = UrlHelper.GetLastPath(this.HttpContext.Request.Path);
var (title, keywords, description, canonical) = UrlHelper.GetSeoData(path);
}
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/" />
@*SEO*@
<component type="typeof(TitleTagComponent)" render-mode="Static" param-Content=@title />
<component type="typeof(KeywordsMetaTagComponent)" render-mode="Static" param-Content=@keywords />
<component type="typeof(DescriptionMetaTagComponent)" render-mode="Static" param-Content=@description />
<component type="typeof(CanonicalMetaTagComponent)" render-mode="Static" param-Content=@canonical />
@*Extra head tag info*@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="index, follow">
<meta name="author" content="App.com">
<meta name="copyright" lang="en" content="App.com">
@*Site icons*@
<link rel="icon" href="favicon.ico" type="image/x-icon">
@*External CSS*@
<link rel="stylesheet" href="css/bootstrap.min.css" />
</head>
<body>
<component type="typeof(App)" render-mode="ServerPrerendered" />
<div id="blazor-error-ui">
<environment include="Staging,Production">
Server connection error. Refresh the page.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss"></a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
- 文件“TitleTagComponent.cs”
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace App.Components {
public class TitleTagComponent : ComponentBase {
[Parameter]
public string Content { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder) {
base.BuildRenderTree(builder);
builder.OpenElement(0, "title");
builder.AddContent(1, Content ?? string.Empty);
builder.CloseElement();
}
}
}
- 文件“KeywordsMetaTagComponent.cs”
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace App.Components {
public class KeywordsMetaTagComponent : ComponentBase {
[Parameter]
public string Content { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder) {
base.BuildRenderTree(builder);
builder.OpenElement(0, "meta");
builder.AddAttribute(1, "name", "keywords");
builder.AddAttribute(2, "content", Content ?? string.Empty);
builder.CloseElement();
}
}
}
- 文件“DescriptionMetaTagComponent.cs”
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace App.Components {
public class DescriptionMetaTagComponent : ComponentBase {
[Parameter]
public string Content { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder) {
base.BuildRenderTree(builder);
builder.OpenElement(0, "meta");
builder.AddAttribute(1, "name", "description");
builder.AddAttribute(2, "content", Content ?? string.Empty);
builder.CloseElement();
}
}
}
- 文件“CanonicalMetaTagComponent.cs”
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace App.Components {
public class CanonicalMetaTagComponent : ComponentBase {
[Parameter]
public string Content { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder) {
base.BuildRenderTree(builder);
builder.OpenElement(0, "link");
builder.AddAttribute(1, "rel", "canonical");
builder.AddAttribute(2, "href", Content ?? string.Empty);
builder.CloseElement();
}
}
}
- 文件“UrlHelper.cs”
using System;
using System.Text.RegularExpressions;
namespace App.Helpers {
public static class UrlHelper {
/// <summary>
/// Regular expression to get all paths from short URL path without "/". Moreover, the first and last "/" may or may not be present.
/// Example: from the string "path1/path2/path3" - path1, path2 and path3 will be selected
/// </summary>
private static Regex ShortUrlPathRegex = new(@"^((?:/?)([\w\s\.-]+)*)*/*", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant);
/// <summary>
/// Retrieving the last path from short URL path based on a regular expression.
/// </summary>
/// <param name="path">Short URL path</param>
/// <returns>Last path from short URL path</returns>
public static string GetLastPath(string path) {
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
try {
var match = ShortUrlPathRegex.Match(path);
if (!match.Success)
return string.Empty;
return match.Groups[2].Value;
}
catch (Exception) {
return string.Empty;
}
}
/// <summary>
/// Get data for SEO (title, keywords, description, canonical) depending on the path URL
/// </summary>
/// <param name="path">URL path</param>
/// <returns>
/// Tuple:
/// item1 - title
/// item2 - keywords
/// item3 - description
/// item4 - canonical
/// </returns>
public static (string, string, string, string) GetSeoData(string path) {
string title = "App";
string keywords = "app1, app2, app3";
string description = "Default App description.";
string canonical = "https://app.com";
if (string.IsNullOrWhiteSpace(path))
return (title, keywords, description, canonical);
switch (path.ToLower()) {
case "page1":
title = "page1 on App.com";
keywords = "page1, page1, page1";
description = "Description for page1";
canonical = "https://app.com/page1";
return (title, keywords, description, canonical);
case "page2":
title = "page2 on App.com";
keywords = "page2, page2, page2";
description = "Description for page2";
canonical = "https://app.com/page2";
return (title, keywords, description, canonical);
case "page3":
title = "page3 on App.com";
keywords = "page3, page3, page3";
description = "Description for page3";
canonical = "https://app.com/page3";
return (title, keywords, description, canonical);
}
return (title, keywords, description, canonical);
}
}
}
根据 Brad Bamford 的评论,我添加了一个 GetSeoDataDB 方法示例,其中我将 switch 替换为对数据库的查询,以便根据使用 Dapper 的页面从数据库中动态检索 SEO 数据:
public static async Task<(string, string, string, string)> GetSeoDataDB(string path) {
string title = "App";
string keywords = "app1, app2, app3";
string description = "Default App description.";
string canonical = "https://app.com";
if (string.IsNullOrWhiteSpace(path))
return (title, keywords, description, canonical);
var ConnectionString = "";
var sql = $@"
SELECT *
FROM `SeoTable`
WHERE `Page` = '{path}'
;";
using IDbConnection db = new MySqlConnection(ConnectionString);
try {
var result = await db.QueryFirstOrDefaultAsync<SeoModel>(sql);
title = string.Join(", ", result.Title);
keywords = string.Join(", ", result.Keywords);
description = string.Join(", ", result.Description);
canonical = string.Join(", ", result.Canonical);
}
catch (Exception ex) {
return (title, keywords, description, canonical);
}
return (title, keywords, description, canonical);
}
这是参考 #10450。
目标: 使用来自页面本身的数据为 SEO 和 Open Graph 目的设置元标记(标题、描述等)。使用 Javascript 互操作将无济于事,因为无法抓取页面。
我使用了@honkmother 的 suggestion 并将基本组件进一步向上移动以封装 <html>
标记,但由于某种原因,这影响了路由。所有链接都以 ~/
开头,我似乎不明白为什么。
我已经创建了一个示例 repo here 如果有人有兴趣看一看。
_Hosts.cshtml
@page "/"
@namespace BlazorMetaTags.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
<!DOCTYPE html>
<component type="typeof(AppBase)" render-mode="ServerPrerendered" />
AppBase.cs
using BlazorMetaTags.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace BlazorMetaTags
{
public class AppBase : ComponentBase
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "html");
builder.AddAttribute(1, "lang", "en");
builder.OpenElement(2, "head");
builder.OpenComponent<Head>(3);
builder.CloseComponent();
builder.CloseElement();
builder.OpenElement(3, "body");
builder.OpenElement(4, "app");
builder.OpenComponent<App>(5);
builder.CloseComponent();
builder.CloseElement();
builder.OpenComponent<Body>(6);
builder.CloseComponent();
builder.AddMarkupContent(7, " <script src='_framework/blazor.server.js'></script>");
builder.CloseElement();
builder.CloseElement();
}
}
public class MetaTags
{
public string Title { get; set; } = "";
public string Description { get; set; } = "";
}
}
Head.razor 组件设置元标签
@inject AppState _appState
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@_appState.MetaTags.Title</title>
<meta name="description" content="@_appState.MetaTags.Description">
<base href="~/" />
<link rel="stylesheet" href="/css/bootstrap/bootstrap.min.css" />
<link href="/css/site.css" rel="stylesheet" />
</head>
@code {
protected override async Task OnInitializedAsync()
{
_appState.OnChange += StateHasChanged;
}
}
Body.razor
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss"></a>
</div>
AppState.cs
using System;
namespace BlazorMetaTags
{
public class AppState
{
public MetaTags MetaTags { get; private set; } = new MetaTags();
public event Action OnChange;
public void SetMetaTags(MetaTags metatags)
{
MetaTags = metatags;
NotifyStateChanged();
}
private void NotifyStateChanged() => OnChange?.Invoke();
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddScoped<AppState>();
services.AddSingleton<WeatherForecastService>();
}
根据
<base href="~/" />
至
<base href="/" />
这对我有用!
我提出我对这个问题的决定。这个解决方案有几个优点:
- SEO 数据将始终是最新的,无需使用 Blazor Server 的“_framework/blazor.server.js”,这将使您能够获取各种机器人程序(包括 Postman)的最新 SEO 数据、curl 和其他非浏览器程序。
- 无需使用 DevExpress Free Blazor Utilities 和 Dev Tools 或其他类似的外部库。
- 不需要重载整个 head 标签。因为当你重载整个 head 标签并使用外部 CSS 样式时,会发生以下情况:在第一次渲染期间,CSS 样式不起作用,但开始在第二次渲染后工作,导致页面闪烁,因为 CSS 样式在第二次渲染后开始工作。
这是我经过测试的有效解决方案:
- 文件“_Host.cshtml”:
@page "/"
@namespace App.Pages
@using App.Components
@using App.Helpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
string path = UrlHelper.GetLastPath(this.HttpContext.Request.Path);
var (title, keywords, description, canonical) = UrlHelper.GetSeoData(path);
}
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/" />
@*SEO*@
<component type="typeof(TitleTagComponent)" render-mode="Static" param-Content=@title />
<component type="typeof(KeywordsMetaTagComponent)" render-mode="Static" param-Content=@keywords />
<component type="typeof(DescriptionMetaTagComponent)" render-mode="Static" param-Content=@description />
<component type="typeof(CanonicalMetaTagComponent)" render-mode="Static" param-Content=@canonical />
@*Extra head tag info*@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="index, follow">
<meta name="author" content="App.com">
<meta name="copyright" lang="en" content="App.com">
@*Site icons*@
<link rel="icon" href="favicon.ico" type="image/x-icon">
@*External CSS*@
<link rel="stylesheet" href="css/bootstrap.min.css" />
</head>
<body>
<component type="typeof(App)" render-mode="ServerPrerendered" />
<div id="blazor-error-ui">
<environment include="Staging,Production">
Server connection error. Refresh the page.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss"></a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
- 文件“TitleTagComponent.cs”
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace App.Components {
public class TitleTagComponent : ComponentBase {
[Parameter]
public string Content { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder) {
base.BuildRenderTree(builder);
builder.OpenElement(0, "title");
builder.AddContent(1, Content ?? string.Empty);
builder.CloseElement();
}
}
}
- 文件“KeywordsMetaTagComponent.cs”
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace App.Components {
public class KeywordsMetaTagComponent : ComponentBase {
[Parameter]
public string Content { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder) {
base.BuildRenderTree(builder);
builder.OpenElement(0, "meta");
builder.AddAttribute(1, "name", "keywords");
builder.AddAttribute(2, "content", Content ?? string.Empty);
builder.CloseElement();
}
}
}
- 文件“DescriptionMetaTagComponent.cs”
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace App.Components {
public class DescriptionMetaTagComponent : ComponentBase {
[Parameter]
public string Content { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder) {
base.BuildRenderTree(builder);
builder.OpenElement(0, "meta");
builder.AddAttribute(1, "name", "description");
builder.AddAttribute(2, "content", Content ?? string.Empty);
builder.CloseElement();
}
}
}
- 文件“CanonicalMetaTagComponent.cs”
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace App.Components {
public class CanonicalMetaTagComponent : ComponentBase {
[Parameter]
public string Content { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder) {
base.BuildRenderTree(builder);
builder.OpenElement(0, "link");
builder.AddAttribute(1, "rel", "canonical");
builder.AddAttribute(2, "href", Content ?? string.Empty);
builder.CloseElement();
}
}
}
- 文件“UrlHelper.cs”
using System;
using System.Text.RegularExpressions;
namespace App.Helpers {
public static class UrlHelper {
/// <summary>
/// Regular expression to get all paths from short URL path without "/". Moreover, the first and last "/" may or may not be present.
/// Example: from the string "path1/path2/path3" - path1, path2 and path3 will be selected
/// </summary>
private static Regex ShortUrlPathRegex = new(@"^((?:/?)([\w\s\.-]+)*)*/*", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant);
/// <summary>
/// Retrieving the last path from short URL path based on a regular expression.
/// </summary>
/// <param name="path">Short URL path</param>
/// <returns>Last path from short URL path</returns>
public static string GetLastPath(string path) {
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
try {
var match = ShortUrlPathRegex.Match(path);
if (!match.Success)
return string.Empty;
return match.Groups[2].Value;
}
catch (Exception) {
return string.Empty;
}
}
/// <summary>
/// Get data for SEO (title, keywords, description, canonical) depending on the path URL
/// </summary>
/// <param name="path">URL path</param>
/// <returns>
/// Tuple:
/// item1 - title
/// item2 - keywords
/// item3 - description
/// item4 - canonical
/// </returns>
public static (string, string, string, string) GetSeoData(string path) {
string title = "App";
string keywords = "app1, app2, app3";
string description = "Default App description.";
string canonical = "https://app.com";
if (string.IsNullOrWhiteSpace(path))
return (title, keywords, description, canonical);
switch (path.ToLower()) {
case "page1":
title = "page1 on App.com";
keywords = "page1, page1, page1";
description = "Description for page1";
canonical = "https://app.com/page1";
return (title, keywords, description, canonical);
case "page2":
title = "page2 on App.com";
keywords = "page2, page2, page2";
description = "Description for page2";
canonical = "https://app.com/page2";
return (title, keywords, description, canonical);
case "page3":
title = "page3 on App.com";
keywords = "page3, page3, page3";
description = "Description for page3";
canonical = "https://app.com/page3";
return (title, keywords, description, canonical);
}
return (title, keywords, description, canonical);
}
}
}
根据 Brad Bamford 的评论,我添加了一个 GetSeoDataDB 方法示例,其中我将 switch 替换为对数据库的查询,以便根据使用 Dapper 的页面从数据库中动态检索 SEO 数据:
public static async Task<(string, string, string, string)> GetSeoDataDB(string path) {
string title = "App";
string keywords = "app1, app2, app3";
string description = "Default App description.";
string canonical = "https://app.com";
if (string.IsNullOrWhiteSpace(path))
return (title, keywords, description, canonical);
var ConnectionString = "";
var sql = $@"
SELECT *
FROM `SeoTable`
WHERE `Page` = '{path}'
;";
using IDbConnection db = new MySqlConnection(ConnectionString);
try {
var result = await db.QueryFirstOrDefaultAsync<SeoModel>(sql);
title = string.Join(", ", result.Title);
keywords = string.Join(", ", result.Keywords);
description = string.Join(", ", result.Description);
canonical = string.Join(", ", result.Canonical);
}
catch (Exception ex) {
return (title, keywords, description, canonical);
}
return (title, keywords, description, canonical);
}