Skip to content

Commit fd0499b

Browse files
committed
Tidied up guide paage, added discord link
1 parent b40f236 commit fd0499b

5 files changed

Lines changed: 123 additions & 112 deletions

File tree

Components/Layout/MainLayout.razor

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@inherits LayoutComponentBase
2+
@inject NavigationManager nav
23

3-
<a class="skip-link" href="#main">Skip to main content</a>
4+
<a class="skip-link" href="@skipLink">Skip to main content</a>
45

56
<header class="site-header">
67
<div class="site-header-inner">
@@ -34,3 +35,12 @@
3435
<span class="visually-hidden"> (opens in a new tab)</span>
3536
</a>
3637
</nav>
38+
39+
@code {
40+
41+
private string skipLink => nav.Uri + "#main";
42+
43+
44+
45+
46+
}

Components/Pages/GuidePage.razor

Lines changed: 106 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -8,74 +8,61 @@
88
@inject IHttpContextAccessor HttpContextAccessor
99
@inject SiteBranding Brand
1010

11-
<PageTitle>@_pageTitleFull</PageTitle>
11+
<PageTitle>@PageTitleFull</PageTitle>
1212

1313
<HeadContent>
14-
@* Basic *@
1514
<meta name="application-name" content="@Brand.SiteName" />
1615
@if (!string.IsNullOrWhiteSpace(Brand.ThemeColor))
1716
{
1817
<meta name="theme-color" content="@Brand.ThemeColor" />
1918
}
2019

21-
@if (!string.IsNullOrWhiteSpace(_canonical))
20+
@if (!string.IsNullOrWhiteSpace(Canonical))
2221
{
23-
<link rel="canonical" href="@_canonical" />
24-
<meta property="og:url" content="@_canonical" />
22+
<link rel="canonical" href="@Canonical" />
23+
<meta property="og:url" content="@Canonical" />
2524
}
2625

27-
@if (!string.IsNullOrWhiteSpace(_description))
26+
@if (!string.IsNullOrWhiteSpace(Description))
2827
{
29-
<meta name="description" content="@_description" />
28+
<meta name="description" content="@Description" />
3029
}
3130

32-
@* Social cards *@
3331
<meta property="og:site_name" content="@Brand.SiteName" />
34-
<meta property="og:title" content="@_pageTitleFull" />
32+
<meta property="og:title" content="@PageTitleFull" />
3533
<meta property="og:type" content="article" />
3634

37-
@if (!string.IsNullOrWhiteSpace(_description))
35+
@if (!string.IsNullOrWhiteSpace(Description))
3836
{
39-
<meta property="og:description" content="@_description" />
37+
<meta property="og:description" content="@Description" />
4038
}
4139

42-
@{
43-
var ogImage = !string.IsNullOrWhiteSpace(_ogImage) ? _ogImage : Brand.DefaultOgImage;
44-
var squareImage = !string.IsNullOrWhiteSpace(_squareImage) ? _squareImage : "";
45-
}
46-
47-
48-
49-
@if (!string.IsNullOrWhiteSpace(squareImage))
40+
@if (!string.IsNullOrWhiteSpace(SquareImageAbs))
5041
{
51-
<meta property="og:image" content="@ToAbsolute(squareImage)" />
42+
<meta property="og:image" content="@SquareImageAbs" />
5243
<meta property="og:image:width" content="1024" />
5344
<meta property="og:image:height" content="1024" />
54-
5545
}
5646

57-
58-
<meta name="twitter:title" content="@_pageTitleFull" />
59-
@if (!string.IsNullOrWhiteSpace(_description))
47+
<meta name="twitter:title" content="@PageTitleFull" />
48+
@if (!string.IsNullOrWhiteSpace(Description))
6049
{
61-
<meta name="twitter:description" content="@_description" />
50+
<meta name="twitter:description" content="@Description" />
6251
}
6352

6453
@if (!string.IsNullOrWhiteSpace(Brand.TwitterHandle))
6554
{
6655
<meta name="twitter:site" content="@Brand.TwitterHandle" />
6756
}
6857

69-
@if (_noIndex)
58+
@if (NoIndex)
7059
{
7160
<meta name="robots" content="noindex,nofollow" />
7261
}
7362

74-
@* JSON-LD *@
7563
<script type="application/ld+json">@_jsonLd</script>
7664
</HeadContent>
7765

78-
7966
@if (_notFound)
8067
{
8168
<h1>Not found</h1>
@@ -93,18 +80,41 @@ else
9380
<div class="doc-hero2-inner">
9481
<div class="doc-hero2-meta">
9582
<p class="doc-kicker">Guide</p>
96-
<h1 id="doc-title" class="doc-title">@_title</h1>
83+
<h1 id="doc-title" class="doc-title">@Title</h1>
9784

98-
@if (!string.IsNullOrWhiteSpace(_description))
85+
@if (!string.IsNullOrWhiteSpace(Description))
9986
{
100-
<p class="doc-lead">@_description</p>
87+
<p class="doc-lead">@Description</p>
10188
}
10289

10390
<div class="doc-actions" role="navigation" aria-label="Document actions">
104-
<a class="doc-action" href="/guide">All guides</a>
105-
@if (!string.IsNullOrWhiteSpace(_canonical))
91+
<a class="doc-action" href="/guide">
92+
<i class="bi bi-file-earmark-text"></i>
93+
All guides
94+
</a>
95+
96+
@if (!string.IsNullOrWhiteSpace(Canonical))
10697
{
107-
<a class="doc-action" href="@_canonical">Permalink</a>
98+
<a class="doc-action" href="@Canonical">
99+
<i class="bi bi-link-45deg"></i>
100+
Permalink
101+
</a>
102+
}
103+
104+
@if (!string.IsNullOrWhiteSpace(LeaderboardUrl))
105+
{
106+
<a class="doc-action" href="@LeaderboardUrl">
107+
<i class="bi bi-trophy"></i>
108+
Leaderboard
109+
</a>
110+
}
111+
112+
@if (!string.IsNullOrWhiteSpace(DiscordUrl))
113+
{
114+
<a class="doc-action" href="@DiscordUrl">
115+
<i class="bi bi-discord"></i>
116+
Discord
117+
</a>
108118
}
109119
</div>
110120
</div>
@@ -155,25 +165,42 @@ else
155165
</div>
156166
}
157167

158-
159168
@code {
160169
[Parameter] public string? slug { get; set; }
161170

162-
private string _title = "Loading...";
163-
private string? _description;
164-
private string? _ogImage;
165-
private string? _squareImage;
166-
167-
private string? _canonical;
168-
private bool _noIndex;
169-
171+
private MarkdownDoc? _doc;
170172
private bool _notFound;
173+
171174
private string? _html;
175+
private string _canonical = "";
172176
private string _pageTitleFull = "Loading...";
173177
private string _jsonLd = "";
178+
174179
private NavNode? _nav;
175180
private string _navRootSlug = "";
176-
private List<DocEntry> _children = new();
181+
182+
// ---- computed doc fields ----
183+
private string Title => _doc?.Title ?? (_notFound ? "Not found" : "Loading...");
184+
private string? Description => _doc?.Description;
185+
private bool NoIndex => _doc?.NoIndex ?? _notFound;
186+
187+
private string Canonical => _canonical;
188+
private string PageTitleFull => _pageTitleFull;
189+
190+
private string? LeaderboardUrl => _doc?.LeaderboardUrl;
191+
private string? DiscordUrl => _doc?.DiscordURL;
192+
193+
private string? OgImagePath => !string.IsNullOrWhiteSpace(_doc?.OgImage) ? _doc!.OgImage : Brand.DefaultOgImage;
194+
private string? SquareImagePath => _doc?.SquareImage;
195+
196+
private string? OgImageAbs => !string.IsNullOrWhiteSpace(OgImagePath) ? ToAbsolute(OgImagePath!) : null;
197+
private string? SquareImageAbs => !string.IsNullOrWhiteSpace(SquareImagePath) ? ToAbsolute(SquareImagePath!) : null;
198+
199+
private string HeroStyle
200+
=> !string.IsNullOrWhiteSpace(OgImageAbs)
201+
? $"--hero-image: url('{OgImageAbs}');"
202+
: "";
203+
177204
private RenderFragment RenderNav(NavNode node) => @<ul class="nav-list nav-list--nested">
178205
@foreach (var child in node.Children)
179206
{
@@ -192,94 +219,67 @@ else
192219
</li>
193220
}
194221
</ul>;
195-
private string HeroStyle
196-
=> string.IsNullOrWhiteSpace(_ogImage)
197-
? ""
198-
: $"--hero-image: url('{ToAbsolute(_ogImage)}');";
199-
private string ToGuideRoute(string contentSlug)
222+
223+
protected override async Task OnParametersSetAsync()
224+
{
225+
var ctx = HttpContextAccessor.HttpContext!;
226+
var host = ctx.Request.Host.Host;
227+
228+
var (doc, _) = await Store.TryGetByRequestAsync(slug, host);
229+
_doc = doc;
230+
231+
var normalized = (slug ?? "").Trim('/');
232+
_navRootSlug = Store.GetNavRootSlug(normalized);
233+
_nav = Store.BuildNavTree(_navRootSlug, normalized);
234+
235+
_notFound = doc is null;
236+
237+
// canonical works even for not-found
238+
_canonical = _notFound
239+
? BuildCanonical(ctx, slug ?? "")
240+
: BuildCanonical(ctx, doc!.Slug, doc.Canonical);
241+
242+
_pageTitleFull = $"{Title} | {Brand.SiteName}";
243+
244+
_jsonLd = BuildJsonLd(
245+
_pageTitleFull,
246+
Description ?? Brand.Tagline,
247+
_canonical,
248+
OgImageAbs
249+
);
250+
251+
_html = _notFound ? null : Renderer.ToHtml(doc!.Html);
252+
}
253+
254+
private string ToGuideRoute(string contentSlug)
200255
{
201256
contentSlug = (contentSlug ?? "").Trim('/').Replace('\\', '/');
202257

203-
// the page route is already /guide/{**slug}
204258
if (contentSlug.Equals("guide", StringComparison.OrdinalIgnoreCase))
205259
return "/guide";
206260

207261
if (contentSlug.StartsWith("guide/", StringComparison.OrdinalIgnoreCase))
208262
contentSlug = contentSlug["guide/".Length..];
209263

210-
// If the slug accidentally contains ".md", remove it
211264
if (contentSlug.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
212265
contentSlug = contentSlug[..^3].TrimEnd('/');
213266

214-
// Map "timer/index" to "/guide/timer" (nicer URLs)
215267
if (contentSlug.EndsWith("/index", StringComparison.OrdinalIgnoreCase))
216268
contentSlug = contentSlug[..^"/index".Length].TrimEnd('/');
217269

218270
return string.IsNullOrWhiteSpace(contentSlug) ? "/guide" : $"/guide/{contentSlug}";
219271
}
220272

221-
protected override async Task OnParametersSetAsync()
222-
{
223-
var ctx = HttpContextAccessor.HttpContext!;
224-
var host = ctx.Request.Host.Host;
225-
226-
var (doc, _) = await Store.TryGetByRequestAsync(slug, host);
227-
var normalized = (slug ?? "").Trim('/');
228-
229-
230-
_navRootSlug = Store.GetNavRootSlug(normalized);
231-
_nav = Store.BuildNavTree(_navRootSlug, normalized);
232-
233-
234-
if (doc is null)
235-
{
236-
_notFound = true;
237-
_title = "Not found";
238-
_description = null;
239-
_ogImage = null;
240-
_squareImage = doc.SquareImage;
241-
242-
_noIndex = true;
243-
_canonical = BuildCanonical(ctx, slug ?? "");
244-
return;
245-
}
246-
247-
_notFound = false;
248-
_title = doc.Title;
249-
_description = doc.Description;
250-
_ogImage = doc.OgImage;
251-
_squareImage = doc.SquareImage;
252-
253-
_noIndex = doc.NoIndex;
254-
_canonical = BuildCanonical(ctx, doc.Slug, doc.Canonical);
255-
256-
_pageTitleFull = $"{_title} | {Brand.SiteName}";
257-
258-
var ogAbs = !string.IsNullOrWhiteSpace(_ogImage)
259-
? ToAbsolute(_ogImage)
260-
: (!string.IsNullOrWhiteSpace(Brand.DefaultOgImage) ? ToAbsolute(Brand.DefaultOgImage) : null);
261-
262-
_jsonLd = BuildJsonLd(_pageTitleFull, _description ?? Brand.Tagline, _canonical, ogAbs);
263-
264-
_html = Renderer.ToHtml(doc.Html);
265-
266-
267-
}
268-
269273
private string BuildCanonical(HttpContext ctx, string? contentSlug, string? canonicalOverride = null)
270274
{
271275
if (!string.IsNullOrWhiteSpace(canonicalOverride))
272276
return canonicalOverride.Trim();
273277

274278
var baseUrl = $"{ctx.Request.Scheme}://{ctx.Request.Host}".TrimEnd('/');
275-
276-
// Use the same routing rules as the UI links
277-
var path = ToGuideRoute(contentSlug ?? ""); // returns "/guide" or "/guide/{slug}"
278-
279+
var path = ToGuideRoute(contentSlug ?? "");
279280
return baseUrl + path;
280281
}
281282

282-
283283
private string ToAbsolute(string urlOrPath)
284284
{
285285
if (string.IsNullOrWhiteSpace(urlOrPath)) return urlOrPath;
@@ -302,15 +302,14 @@ private string BuildJsonLd(string pageTitle, string? description, string canonic
302302
var siteName = Brand.SiteName;
303303
var tagline = Brand.Tagline;
304304

305-
// WebSite + WebPage/Article
306305
return $@"
307306
{{
308307
""@context"": ""https://schema.org"",
309308
""@graph"": [
310309
{{
311310
""@type"": ""WebSite"",
312311
""name"": ""{J(siteName)}"",
313-
""url"": ""{J($"{HttpContextAccessor.HttpContext!.Request.Scheme}://{HttpContextAccessor.HttpContext!.Request.Host}/")}"" ,
312+
""url"": ""{J($"{HttpContextAccessor.HttpContext!.Request.Scheme}://{HttpContextAccessor.HttpContext!.Request.Host}/")}"",
314313
""description"": ""{J(tagline)}""
315314
}},
316315
{{
@@ -323,5 +322,4 @@ private string BuildJsonLd(string pageTitle, string? description, string canonic
323322
]
324323
}}".Trim();
325324
}
326-
327325
}

Data/MarkdownDoc.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public sealed class MarkdownDoc
88

99
public string? GameName { get; init; }
1010
public string? LeaderboardUrl { get; init; }
11+
public string? DiscordURL { get; init; }
1112
public string? TimingMethod { get; init; }
1213
public string? DownpatchRequired { get; init; }
1314
public string? AllowedVersions { get; init; }

0 commit comments

Comments
 (0)