121291eeed
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>
86 lines
2.6 KiB
C#
86 lines
2.6 KiB
C#
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;
|
|
}
|
|
}
|