我就这个问题提出我的决定。此解决方案有几个优点:
- SEO 数据将始终是最新的,无需使用 Blazor Server 的“_framework/blazor.server.js”,这将允许您获取各种机器人程序的最新 SEO 数据,包括 Postman、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 的页面从 DB 中动态检索 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);
}