Implement URL slugs (#931)

* Implement URL slugs for Users and Slots

* Fix extra spaces in slot slugs
This commit is contained in:
Josh 2023-10-29 15:30:43 -05:00 committed by GitHub
parent aea66b4a74
commit 1eb3bfa2ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 55 additions and 8 deletions

View file

@ -0,0 +1,33 @@
using System.Text.RegularExpressions;
using System.Web;
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
namespace LBPUnion.ProjectLighthouse.Servers.Website.Extensions;
public static partial class SlugExtensions
{
[GeneratedRegex("[^a-zA-Z0-9 ]")]
private static partial Regex ValidSlugCharactersRegex();
[GeneratedRegex(@"[\s]{2,}")]
private static partial Regex WhitespaceRegex();
/// <summary>
/// Generates a URL slug that only contains alphanumeric characters
/// with spaces replaced with dashes
/// </summary>
/// <param name="slot">The slot to generate the slug for</param>
/// <returns>A string containing the url slug for this slot</returns>
public static string GenerateSlug(this SlotEntity slot) =>
slot.Name.Length == 0
? "unnamed-level"
: WhitespaceRegex().Replace(ValidSlugCharactersRegex().Replace(HttpUtility.HtmlDecode(slot.Name), ""), " ").Replace(" ", "-").ToLower();
/// <summary>
/// Generates a URL slug for the given user
/// </summary>
/// <param name="user">The user to generate the slug for</param>
/// <returns>A string containing the url slug for this user</returns>
public static string GenerateSlug(this UserEntity user) => user.Username.ToLower();
}

View file

@ -12,7 +12,7 @@
string userStatus = includeStatus ? Model.GetStatus(Database).ToTranslatedString(language, timeZone) : ""; string userStatus = includeStatus ? Model.GetStatus(Database).ToTranslatedString(language, timeZone) : "";
} }
<a href="/user/@Model.UserId" title="@userStatus" class="user-link"> <a href="/user/@Model.UserId/@Model.GenerateSlug()" title="@userStatus" class="user-link">
<img src="/gameAssets/@Model.WebsiteAvatarHash" alt=""/> <img src="/gameAssets/@Model.WebsiteAvatarHash" alt=""/>
@if (Model.IsModerator) @if (Model.IsModerator)

View file

@ -53,7 +53,7 @@
@if (showLink) @if (showLink)
{ {
<h2> <h2>
<a href="~/slot/@Model.SlotId">@slotName</a> <i class="@Model.GetLevelLockIcon()"></i> <a href="~/slot/@Model.SlotId/@Model.GenerateSlug()">@slotName</a> <i class="@Model.GetLevelLockIcon()"></i>
</h2> </h2>
} }
else else
@ -68,7 +68,7 @@
@if (showLink) @if (showLink)
{ {
<h3> <h3>
<a href="~/slot/@Model.SlotId">@slotName</a> <i class="@Model.GetLevelLockIcon()"></i> <a href="~/slot/@Model.SlotId/@Model.GenerateSlug()">@slotName</a> <i class="@Model.GetLevelLockIcon()"></i>
</h3> </h3>
} }
else else

View file

@ -22,7 +22,7 @@
@if (showLink) @if (showLink)
{ {
<h2 style="margin-bottom: 2px;"> <h2 style="margin-bottom: 2px;">
<a href="~/user/@Model.UserId">@Model.Username</a> <a href="~/user/@Model.UserId/@Model.GenerateSlug()">@Model.Username</a>
@if (Model.IsModerator) @if (Model.IsModerator)
{ {
<span class="profile-tag ui label @Model.PermissionLevel.ToHtmlColor()"> <span class="profile-tag ui label @Model.PermissionLevel.ToHtmlColor()">

View file

@ -1,4 +1,4 @@
@page "/slot/{id:int}" @page "/slot/{id:int}/{slug?}"
@using System.Web @using System.Web
@using LBPUnion.ProjectLighthouse.Database @using LBPUnion.ProjectLighthouse.Database
@using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Extensions

View file

@ -1,6 +1,7 @@
#nullable enable #nullable enable
using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Servers.Website.Extensions;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Level;
@ -28,7 +29,7 @@ public class SlotPage : BaseLayout
public SlotPage(DatabaseContext database) : base(database) public SlotPage(DatabaseContext database) : base(database)
{} {}
public async Task<IActionResult> OnGet([FromRoute] int id) public async Task<IActionResult> OnGet([FromRoute] int id, string? slug)
{ {
SlotEntity? slot = await this.Database.Slots.Include(s => s.Creator) SlotEntity? slot = await this.Database.Slots.Include(s => s.Creator)
.Where(s => s.Type == SlotType.User || (this.User != null && this.User.PermissionLevel >= PermissionLevel.Moderator)) .Where(s => s.Type == SlotType.User || (this.User != null && this.User.PermissionLevel >= PermissionLevel.Moderator))
@ -45,6 +46,12 @@ public class SlotPage : BaseLayout
if ((slot.Hidden || slot.SubLevel && (this.User == null && this.User != slot.Creator)) && !(this.User?.IsModerator ?? false)) if ((slot.Hidden || slot.SubLevel && (this.User == null && this.User != slot.Creator)) && !(this.User?.IsModerator ?? false))
return this.NotFound(); return this.NotFound();
string slotSlug = slot.GenerateSlug();
if (slug == null || slotSlug != slug)
{
return this.Redirect($"~/slot/{id}/{slotSlug}");
}
this.Slot = slot; this.Slot = slot;
List<int> blockedUsers = this.User == null List<int> blockedUsers = this.User == null

View file

@ -1,4 +1,4 @@
@page "/user/{userId:int}" @page "/user/{userId:int}/{slug?}"
@using System.Web @using System.Web
@using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Extensions
@using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.Localization.StringLists

View file

@ -1,6 +1,7 @@
#nullable enable #nullable enable
using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Configuration;
using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Database;
using LBPUnion.ProjectLighthouse.Servers.Website.Extensions;
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction;
using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Level;
@ -38,11 +39,17 @@ public class UserPage : BaseLayout
public UserPage(DatabaseContext database) : base(database) public UserPage(DatabaseContext database) : base(database)
{ } { }
public async Task<IActionResult> OnGet([FromRoute] int userId) public async Task<IActionResult> OnGet([FromRoute] int userId, string? slug)
{ {
this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId); this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId);
if (this.ProfileUser == null) return this.NotFound(); if (this.ProfileUser == null) return this.NotFound();
string userSlug = this.ProfileUser.GenerateSlug();
if (slug == null || userSlug != slug)
{
return this.Redirect($"~/user/{userId}/{userSlug}");
}
bool isAuthenticated = this.User != null; bool isAuthenticated = this.User != null;
bool isOwner = this.ProfileUser == this.User || this.User != null && this.User.IsModerator; bool isOwner = this.ProfileUser == this.User || this.User != null && this.User.IsModerator;