Skip to content

Commit 33d7bdd

Browse files
committed
Added SEO to major pages, sitemap.xml and robot.txt. Should make page show up in search engines.
1 parent 06f074e commit 33d7bdd

12 files changed

Lines changed: 349 additions & 0 deletions

File tree

src/Hedin.UI.Demo/App.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@inject IHostEnvironment Env
2+
@using Hedin.UI.Demo.Components
23

34
<!DOCTYPE html>
45
<html lang="en">
@@ -23,6 +24,7 @@
2324

2425
<!-- Choose ONE theme from below (we're using synthwave84 as an example) -->
2526
<link href="@Assets["_content/Brism/prism-one-dark.css"]" rel="stylesheet" />
27+
<SeoComponent />
2628
<HeadOutlet @rendermode="InteractiveServer" />
2729

2830
</head>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
@inject SeoService SeoService
2+
3+
@{
4+
var metadata = SeoService.CurrentMetadata;
5+
var baseUrl = SeoService.GetBaseUrl();
6+
var fullImageUrl = !string.IsNullOrEmpty(metadata.Image) && !metadata.Image.StartsWith("http")
7+
? $"{baseUrl}{metadata.Image}"
8+
: metadata.Image;
9+
var fullUrl = !string.IsNullOrEmpty(metadata.Url)
10+
? (metadata.Url.StartsWith("http") ? metadata.Url : $"{baseUrl}{metadata.Url}")
11+
: baseUrl;
12+
}
13+
14+
<!-- Primary Meta Tags -->
15+
<title>@metadata.Title</title>
16+
<meta name="title" content="@metadata.Title" />
17+
<meta name="description" content="@metadata.Description" />
18+
<meta name="keywords" content="@metadata.Keywords" />
19+
<meta name="author" content="@metadata.Author" />
20+
<meta name="robots" content="index, follow" />
21+
<meta name="language" content="English" />
22+
<meta name="revisit-after" content="7 days" />
23+
24+
<!-- Open Graph / Facebook -->
25+
<meta property="og:type" content="@metadata.Type" />
26+
<meta property="og:url" content="@fullUrl" />
27+
<meta property="og:title" content="@metadata.Title" />
28+
<meta property="og:description" content="@metadata.Description" />
29+
<meta property="og:image" content="@fullImageUrl" />
30+
<meta property="og:site_name" content="@metadata.SiteName" />
31+
<meta property="og:locale" content="@metadata.Locale" />
32+
33+
<!-- Twitter -->
34+
<meta property="twitter:card" content="summary_large_image" />
35+
<meta property="twitter:url" content="@fullUrl" />
36+
<meta property="twitter:title" content="@metadata.Title" />
37+
<meta property="twitter:description" content="@metadata.Description" />
38+
<meta property="twitter:image" content="@fullImageUrl" />
39+
40+
<!-- Additional SEO Meta Tags -->
41+
<meta name="theme-color" content="#1976d2" />
42+
<meta name="msapplication-TileColor" content="#1976d2" />
43+
<meta name="apple-mobile-web-app-capable" content="yes" />
44+
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
45+
<meta name="apple-mobile-web-app-title" content="@metadata.SiteName" />
46+
47+
<!-- Canonical URL -->
48+
<link rel="canonical" href="@fullUrl" />
49+
50+
<!-- Structured Data -->
51+
<script type="application/ld+json">
52+
{
53+
"@@context": "https://schema.org",
54+
"@@type": "WebSite",
55+
"name": "@metadata.SiteName",
56+
"description": "@metadata.Description",
57+
"url": "@baseUrl",
58+
"author": {
59+
"@@type": "Organization",
60+
"name": "@metadata.Author"
61+
},
62+
"potentialAction": {
63+
"@@type": "SearchAction",
64+
"target": "@baseUrl/search?q={search_term_string}",
65+
"query-input": "required name=search_term_string"
66+
}
67+
}
68+
</script>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Hedin.UI.Demo.Services;
3+
4+
namespace Hedin.UI.Demo.Controllers;
5+
6+
[ApiController]
7+
public class SeoController : ControllerBase
8+
{
9+
private readonly SitemapService _sitemapService;
10+
11+
public SeoController(SitemapService sitemapService)
12+
{
13+
_sitemapService = sitemapService;
14+
}
15+
16+
[HttpGet("sitemap.xml")]
17+
public IActionResult GetSitemap()
18+
{
19+
var sitemapXml = _sitemapService.GenerateSitemapXml();
20+
return Content(sitemapXml, "application/xml");
21+
}
22+
23+
[HttpGet("robots.txt")]
24+
public IActionResult GetRobots()
25+
{
26+
var robotsTxt = _sitemapService.GenerateRobotsTxt();
27+
return Content(robotsTxt, "text/plain");
28+
}
29+
}

src/Hedin.UI.Demo/Pages/GettingStarted/GettingStarted.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@attribute [HUIPageSettings("Getting started", 0)]
44

55
@inject MarkdownReader mdReader
6+
@inject SeoService SeoService
67

78
<HUIPage Header="Getting started" PageDescription="This page does not represent the design of Hedin.UI.">
89
<MudContainer MaxWidth="MaxWidth.Large">
@@ -15,4 +16,14 @@
1516
@code {
1617
[CascadingParameter(Name = "DarkMode")] public bool DarkMode { get; set; }
1718
private CodeBlockTheme cbTheme => !DarkMode ? CodeBlockTheme.AtomOneLight : CodeBlockTheme.AtomOneDark;
19+
20+
protected override void OnInitialized()
21+
{
22+
SeoService.SetMetadata(
23+
title: "Getting Started with Hedin UI - Installation Guide",
24+
description: "Learn how to install and set up Hedin UI in your Blazor application. Complete installation guide with examples and best practices.",
25+
keywords: "Hedin UI, Getting Started, Installation, Blazor, MudBlazor, Setup Guide, Tutorial",
26+
type: "article"
27+
);
28+
}
1829
}

src/Hedin.UI.Demo/Pages/Guidelines/Guidelines.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@attribute [Route(PageRoute.Guidelines)]
44
@attribute [HUIPageSettings("Guidelines & Tools", 3)]
55
@inject NavigationManager NavigationManager
6+
@inject SeoService SeoService
67

78

89
<HUIPage Header="Guidelines" PageDescription="These are rules and guidelines to follow when using Hedin.UI and MudBlazor">
@@ -230,6 +231,16 @@ public class PageModel
230231
</HUIPage>
231232

232233
@code {
234+
protected override void OnInitialized()
235+
{
236+
SeoService.SetMetadata(
237+
title: "Hedin UI Guidelines - Design Patterns & Best Practices",
238+
description: "Comprehensive guidelines for using Hedin UI and MudBlazor components. Learn theming, routing, inputs, data grids, and state management best practices.",
239+
keywords: "Hedin UI, Guidelines, Design Patterns, Best Practices, Theming, Routing, MudBlazor, Blazor, UI Components",
240+
type: "article"
241+
);
242+
}
243+
233244
private void HandleOnStateTestClick()
234245
{
235246
NavigationManager.NavigateTo(PageRoute.DemoStateManagement);

src/Hedin.UI.Demo/Pages/Index.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@layout StartLayout
22
@attribute [Route("/")]
3+
@inject SeoService SeoService
34

45
<div>
56

@@ -137,6 +138,16 @@
137138
// public record Stat(string Label, string Value, string Icon);
138139
public record ExternalSource(string Title, string Icon, string Body, string UrlName, string Url);
139140
private string MyLibVersion = typeof(Hedin.UI.HUIAppBar).Assembly.GetName().Version?.ToString() ?? "1.0.0";
141+
142+
protected override void OnInitialized()
143+
{
144+
SeoService.SetMetadata(
145+
title: "Hedin UI - Build Blazor Apps in Minutes",
146+
description: "Hedin UI gives you a polished suite of MudBlazor-based components, themes, and patterns so you can ship UIs faster. Open source Blazor component library.",
147+
keywords: "Blazor, MudBlazor, UI Components, C#, .NET, Web Development, Hedin UI, Open Source, Component Library, Web Framework",
148+
image: "/HedinUI.svg"
149+
);
150+
}
140151
// private Stat[] stats = new[]
141152
// {
142153
// new Stat("Downloads", "15.9 M", Icons.Material.Filled.CloudDownload),

src/Hedin.UI.Demo/Pages/MCP/MCP.razor

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@attribute [Route(PageRoute.MCP)]
22
@attribute [HUIPageSettings("MCP", 6)]
3+
@inject SeoService SeoService
34

45
<MudContainer MaxWidth="MaxWidth.Large" Class="px-3">
56
<HUIPage Header="MCP" PageDescription="Model Context Protocol - Connect Hedin UI to Cursor">
@@ -63,4 +64,14 @@
6364

6465
@code {
6566
[CascadingParameter(Name = "DarkMode")] public bool DarkMode { get; set; }
67+
68+
protected override void OnInitialized()
69+
{
70+
SeoService.SetMetadata(
71+
title: "Hedin UI MCP Integration - Connect to Cursor AI",
72+
description: "Integrate Hedin UI with Cursor AI using Model Context Protocol (MCP). Enhance your development workflow with powerful UI components and design patterns.",
73+
keywords: "Hedin UI, MCP, Model Context Protocol, Cursor AI, Integration, Blazor, Development Tools, AI Assistant",
74+
type: "article"
75+
);
76+
}
6677
}

src/Hedin.UI.Demo/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
app.UseAuthentication();
4242
app.UseAuthorization();
4343

44+
// Map SEO endpoints
45+
app.MapControllers();
46+
4447
app.MapMcp(pattern: "mcp").AllowAnonymous();
4548

4649
app.Run();

src/Hedin.UI.Demo/Services/MCP/Client/McpClientConfiguration.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public static WebApplicationBuilder AddSemanticAi(this WebApplicationBuilder bui
3636
builder.Services.AddMcpServer()
3737
.WithHttpTransport()
3838
.WithTools<HedinUiMcpTools>();
39+
40+
builder.Services.AddScoped<SeoService>();
41+
builder.Services.AddScoped<SitemapService>();
3942
return builder;
4043
}
4144
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System.Reflection;
2+
using Microsoft.AspNetCore.Components;
3+
4+
namespace Hedin.UI.Demo.Services;
5+
6+
public class SeoService
7+
{
8+
private readonly IHttpContextAccessor _httpContextAccessor;
9+
private SeoMetadata _currentMetadata = new();
10+
11+
public SeoService(IHttpContextAccessor httpContextAccessor)
12+
{
13+
_httpContextAccessor = httpContextAccessor;
14+
}
15+
16+
public SeoMetadata CurrentMetadata => _currentMetadata;
17+
18+
public void SetMetadata(SeoMetadata metadata)
19+
{
20+
_currentMetadata = metadata;
21+
}
22+
23+
public void SetMetadata(string? title = null, string? description = null, string? keywords = null,
24+
string? image = null, string? url = null, string? type = "website")
25+
{
26+
_currentMetadata = new SeoMetadata
27+
{
28+
Title = title ?? _currentMetadata.Title,
29+
Description = description ?? _currentMetadata.Description,
30+
Keywords = keywords ?? _currentMetadata.Keywords,
31+
Image = image ?? _currentMetadata.Image,
32+
Url = url ?? GetCurrentUrl(),
33+
Type = type
34+
};
35+
}
36+
37+
public string GetCurrentUrl()
38+
{
39+
var request = _httpContextAccessor.HttpContext?.Request;
40+
if (request == null) return string.Empty;
41+
42+
var scheme = request.Scheme;
43+
var host = request.Host;
44+
var path = request.Path;
45+
var query = request.QueryString;
46+
47+
// Ensure we have a valid URL
48+
var url = $"{scheme}://{host}{path}{query}";
49+
return !string.IsNullOrEmpty(url) ? url : GetBaseUrl();
50+
}
51+
52+
public string GetBaseUrl()
53+
{
54+
var request = _httpContextAccessor.HttpContext?.Request;
55+
if (request == null) return string.Empty;
56+
57+
var scheme = request.Scheme;
58+
var host = request.Host;
59+
60+
return $"{scheme}://{host}";
61+
}
62+
63+
public List<RouteInfo> GetAllRoutableComponents()
64+
{
65+
var routes = new List<RouteInfo>();
66+
var assembly = Assembly.GetExecutingAssembly();
67+
68+
var routableTypes = assembly.GetTypes()
69+
.Where(t => t.IsSubclassOf(typeof(ComponentBase)) &&
70+
t.GetCustomAttributes<RouteAttribute>().Any())
71+
.ToList();
72+
73+
foreach (var type in routableTypes)
74+
{
75+
var routeAttributes = type.GetCustomAttributes<RouteAttribute>();
76+
foreach (var routeAttr in routeAttributes)
77+
{
78+
routes.Add(new RouteInfo
79+
{
80+
Type = type,
81+
Route = routeAttr.Template,
82+
Name = type.Name,
83+
Description = GetComponentDescription(type)
84+
});
85+
}
86+
}
87+
88+
return routes;
89+
}
90+
91+
private string GetComponentDescription(Type componentType)
92+
{
93+
// Try to get description from XML documentation or attributes
94+
var descriptionAttr = componentType.GetCustomAttribute<System.ComponentModel.DescriptionAttribute>();
95+
if (descriptionAttr != null)
96+
return descriptionAttr.Description;
97+
98+
// Generate a generic description - pages should set their own specific descriptions
99+
return $"Hedin UI {componentType.Name} - Component documentation and examples";
100+
}
101+
}
102+
103+
public class SeoMetadata
104+
{
105+
public string Title { get; set; } = "Hedin UI - Build Blazor Apps in Minutes";
106+
public string Description { get; set; } = "Hedin UI gives you a polished suite of MudBlazor-based components, themes, and patterns so you can ship UIs faster.";
107+
public string Keywords { get; set; } = "Blazor, MudBlazor, UI Components, C#, .NET, Web Development, Hedin UI";
108+
public string Image { get; set; } = "/HedinUI.svg";
109+
public string Url { get; set; } = "";
110+
public string Type { get; set; } = "website";
111+
public string Author { get; set; } = "Hedin IT";
112+
public string SiteName { get; set; } = "Hedin UI";
113+
public string Locale { get; set; } = "en_US";
114+
}
115+
116+
public class RouteInfo
117+
{
118+
public Type Type { get; set; } = null!;
119+
public string Route { get; set; } = "";
120+
public string Name { get; set; } = "";
121+
public string Description { get; set; } = "";
122+
}

0 commit comments

Comments
 (0)