Skip to content

Commit

Permalink
feat: Add slug for SEO friendly URL
Browse files Browse the repository at this point in the history
* add BlogPostIdGenerator to generate shorter id

* add SeoFriendlyUrl prop to blog posts

* fix routing parameter

* fix RSS links

* refactor SeoFriendlyUrl property name

* unused NotMapped removed

* URL-Friendly removed from the RSS part

* BlogPostId value generator removed

* GenerateSearchEngineFriendlyUrl fixed

* make searchEngineFriendlyTitle optional

* renaming SearchEngineFriendlyUrl to Slug

* small refactoring in coding style

* removing id from slug generator

* generate Regex with source generators

* ShouldOpenBlogPost test fixed

* tests for slug added

* more testcase added for ShouldGenerateValidSlug

* small issue fixed in MatchIfSpecialCharactersExist Regex
  • Loading branch information
Kamyab7 authored Dec 16, 2023
1 parent 02dfd95 commit 995bf7a
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 13 deletions.
55 changes: 53 additions & 2 deletions src/LinkDotNet.Blog.Domain/BlogPost.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace LinkDotNet.Blog.Domain;

public sealed class BlogPost : Entity
public sealed partial class BlogPost : Entity
{
private BlogPost()
{
Expand Down Expand Up @@ -35,6 +38,54 @@ private BlogPost()

public string TagsAsString => Tags is null ? string.Empty : string.Join(", ", Tags);

public string Slug => GenerateSlug();

private string GenerateSlug()
{
// Remove all accents and make the string lower case.
if (string.IsNullOrWhiteSpace(Title))
return Title;

Title = Title.Normalize(NormalizationForm.FormD);
var chars = Title
.Where(c => CharUnicodeInfo.GetUnicodeCategory(c)
!= UnicodeCategory.NonSpacingMark)
.ToArray();

Title = new string(chars).Normalize(NormalizationForm.FormC);

var slug = Title.ToLower(CultureInfo.CurrentCulture);

// Remove all special characters from the string.
slug = MatchIfSpecialCharactersExist().Replace(slug, "");

// Remove all additional spaces in favour of just one.
slug= MatchIfAdditionalSpacesExist().Replace(slug," ").Trim();

// Replace all spaces with the hyphen.
slug= MatchIfSpaceExist().Replace(slug, "-");

return slug;
}

[GeneratedRegex(
@"[^A-Za-z0-9\s]",
RegexOptions.CultureInvariant,
matchTimeoutMilliseconds: 1000)]
private static partial Regex MatchIfSpecialCharactersExist();

[GeneratedRegex(
@"\s+",
RegexOptions.CultureInvariant,
matchTimeoutMilliseconds: 1000)]
private static partial Regex MatchIfAdditionalSpacesExist();

[GeneratedRegex(
@"\s",
RegexOptions.CultureInvariant,
matchTimeoutMilliseconds: 1000)]
private static partial Regex MatchIfSpaceExist();

public static BlogPost Create(
string title,
string shortDescription,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

Expand Down
8 changes: 4 additions & 4 deletions src/LinkDotNet.Blog.Web/Features/Archive/ArchivePage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@
<ul class="ps-5">
@foreach (var blogPost in yearGroup.OrderByDescending(b => b.UpdatedDate))
{
<li class="pt-1"><a href="/blogPost/@blogPost.Id">@blogPost.Title</a></li>
<li class="pt-1"><a href="/blogPost/@blogPost.Id/@blogPost.Slug">@blogPost.Title</a></li>
}
</ul>
}
}
</div >
</div>

@code {
private IReadOnlyCollection<IGrouping<int, BlogPostPerYear>> blogPostsPerYear;
Expand All @@ -35,7 +35,7 @@
protected override async Task OnInitializedAsync()
{
var blogPosts = await Repository.GetAllByProjectionAsync(
p => new BlogPostPerYear(p.Id, p.Title, p.UpdatedDate),
p => new BlogPostPerYear(p.Id, p.Slug, p.Title, p.UpdatedDate),
p => p.IsPublished);
blogPostCount = blogPosts.Count;
blogPostsPerYear = blogPosts
Expand All @@ -44,5 +44,5 @@
.ToImmutableArray();
}

private sealed record BlogPostPerYear(string Id, string Title, DateTime UpdatedDate);
private sealed record BlogPostPerYear(string Id, string Slug, string Title, DateTime UpdatedDate);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@using System.Net
@using System.Net
@using System.Text.RegularExpressions
@using System.Web
@using LinkDotNet.Blog.Domain
Expand Down Expand Up @@ -43,7 +43,7 @@
<h2></h2>
<p>@MarkdownConverter.ToMarkupString(BlogPost.ShortDescription)</p>
<p class="read-more">
<a href="/blogPost/@BlogPost.Id" aria-label="@BlogPost.Title">Read the whole article</a>
<a href="/blogPost/@BlogPost.Id/@BlogPost.Slug" aria-label="@BlogPost.Title">Read the whole article</a>
</p>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@page "/blogPost/{blogPostId}"
@page "/blogPost/{blogPostId}/{slug?}"
@using Markdig
@using LinkDotNet.Blog.Domain
@using LinkDotNet.Blog.Infrastructure.Persistence
Expand Down Expand Up @@ -73,6 +73,9 @@ else
[Parameter]
public string BlogPostId { get; set; }

[Parameter]
public string Slug { get; set; }

private string OgDataImage => BlogPost.PreviewImageUrlFallback ?? BlogPost.PreviewImageUrl;

private BlogPost BlogPost { get; set; }
Expand Down
23 changes: 22 additions & 1 deletion tests/LinkDotNet.Blog.UnitTests/Domain/BlogPostTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using LinkDotNet.Blog.Domain;
using LinkDotNet.Blog.TestUtilities;
Expand All @@ -24,6 +24,27 @@ public void ShouldUpdateBlogPost()
blogPostToUpdate.PreviewImageUrlFallback.Should().Be("Url2");
blogPostToUpdate.IsPublished.Should().BeTrue();
blogPostToUpdate.Tags.Should().BeNullOrEmpty();
blogPostToUpdate.Slug.Should().NotBeNull();
}

[Theory]
[InlineData("blog title","blog-title")]
[InlineData("blog title", "blog-title")]
[InlineData("blog +title", "blog-title")]
[InlineData("blog/title", "blogtitle")]
[InlineData("blog /title", "blog-title")]
[InlineData("BLOG TITLE", "blog-title")]
[InlineData("àccent", "accent")]
[InlineData("get 100$ quick", "get-100-quick")]
[InlineData("blog,title", "blogtitle")]
[InlineData("blog?!title", "blogtitle")]
[InlineData("blog----title", "blogtitle")]
[InlineData("überaus gut", "uberaus-gut")]
public void ShouldGenerateValidSlug(string title, string expectedSlug)
{
var blogPost = new BlogPostBuilder().WithTitle(title).Build();

blogPost.Slug.Should().Be(expectedSlug);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using LinkDotNet.Blog.TestUtilities;
using LinkDotNet.Blog.Web.Features.Components;
Expand All @@ -17,7 +17,7 @@ public void ShouldOpenBlogPost()

var readMore = cut.Find(".read-more a");

readMore.Attributes.Single(a => a.Name == "href").Value.Should().Be("/blogPost/SomeId");
readMore.Attributes.Single(a => a.Name == "href").Value.Should().Be("/blogPost/SomeId/blogpost");
}

[Fact]
Expand Down

0 comments on commit 995bf7a

Please sign in to comment.