fix(blog): auto-generate slug when admin leaves it empty
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
Allow optional slug on create/update, generate from title with unique fallback, so BackOffice can publish posts without manual slug entry. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.Common;
|
||||
|
||||
public static class BlogSlugHelper
|
||||
{
|
||||
private const int MaxSlugLength = 200;
|
||||
|
||||
public static string? NormalizeOptional(string? slug)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(slug))
|
||||
return null;
|
||||
|
||||
return slug.Trim().ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a URL slug from title (ASCII letters/digits). Persian-only titles yield empty string.
|
||||
/// </summary>
|
||||
public static string GenerateFromTitle(string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
return string.Empty;
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
var lastWasHyphen = false;
|
||||
|
||||
foreach (var c in title.Trim().ToLowerInvariant())
|
||||
{
|
||||
if (c is >= 'a' and <= 'z' or >= '0' and <= '9')
|
||||
{
|
||||
sb.Append(c);
|
||||
lastWasHyphen = false;
|
||||
}
|
||||
else if (c is ' ' or '_' or '-')
|
||||
{
|
||||
if (sb.Length > 0 && !lastWasHyphen)
|
||||
{
|
||||
sb.Append('-');
|
||||
lastWasHyphen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = sb.ToString().Trim('-');
|
||||
return result.Length > MaxSlugLength ? result[..MaxSlugLength].TrimEnd('-') : result;
|
||||
}
|
||||
|
||||
public static string CreateFallbackSlug() =>
|
||||
$"post-{Guid.NewGuid():N}"[..Math.Min(20, MaxSlugLength)];
|
||||
|
||||
public static string ResolveForCreate(string? requestedSlug, string title)
|
||||
{
|
||||
var normalized = NormalizeOptional(requestedSlug);
|
||||
if (!string.IsNullOrEmpty(normalized))
|
||||
return normalized;
|
||||
|
||||
var fromTitle = GenerateFromTitle(title);
|
||||
return !string.IsNullOrEmpty(fromTitle) ? fromTitle : CreateFallbackSlug();
|
||||
}
|
||||
|
||||
public static async Task<string> EnsureUniquePostSlugAsync(
|
||||
IApplicationDbContext context,
|
||||
string slug,
|
||||
long? excludePostId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var candidate = slug;
|
||||
var suffix = 2;
|
||||
|
||||
while (await context.BlogPosts.AnyAsync(
|
||||
p => p.Slug == candidate && !p.IsDeleted && (excludePostId == null || p.Id != excludePostId.Value),
|
||||
cancellationToken))
|
||||
{
|
||||
var suffixStr = $"-{suffix}";
|
||||
var maxBase = MaxSlugLength - suffixStr.Length;
|
||||
var basePart = slug.Length > maxBase ? slug[..maxBase].TrimEnd('-') : slug;
|
||||
candidate = basePart + suffixStr;
|
||||
suffix++;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user