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(); } /// /// Builds a URL slug from title (ASCII letters/digits). Persian-only titles yield empty string. /// 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 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; } }