package useCases import ( "RenewCMS/internal/domain/article" "RenewCMS/internal/domain/image" "RenewCMS/internal/domain/site" ) type CheckArticleSEOUseCase struct { articleRepository article.Repository sites site.Repository images image.Repository } type CheckArticleSEOCommand struct { Body *string Slug *string Excerpt *string SEOTitle *string SEODescription *string CoverImageID article.Nullable[uint32] CoverImageAlt *string SocialTitle *string SocialDescription *string SocialImageID article.Nullable[uint32] CanonicalURL *string Noindex *bool SEODismissed *[]string } func NewCheckArticleSEOUseCase(articleRepository article.Repository, sites site.Repository, images image.Repository) *CheckArticleSEOUseCase { return &CheckArticleSEOUseCase{articleRepository: articleRepository, sites: sites, images: images} } func (u *CheckArticleSEOUseCase) Check(id uint32, command CheckArticleSEOCommand) (article.SEOCheck, error) { saved, err := u.articleRepository.Get(id) if err != nil { return article.SEOCheck{}, err } if err := u.validate(command); err != nil { return article.SEOCheck{}, err } owner, err := u.sites.Get(saved.SiteID) if err != nil { return article.SEOCheck{}, err } candidate := command.applyTo(saved) slugCheck, err := u.checkSlug(candidate) if err != nil { return article.SEOCheck{}, err } assessment := assessSEO(candidate, owner) return article.SEOCheck{Status: assessment.Status, Suggestions: assessment.Suggestions, Slug: slugCheck}, nil } func (u *CheckArticleSEOUseCase) validate(command CheckArticleSEOCommand) error { if command.Slug != nil && *command.Slug != "" && !article.IsValidSlug(*command.Slug) { return article.ErrInvalidSlug } if command.CanonicalURL != nil { if err := article.ValidateCanonicalURL(*command.CanonicalURL); err != nil { return err } } if command.CoverImageID.Set { if err := ensureImageExists(u.images, command.CoverImageID.Value); err != nil { return err } } if command.SocialImageID.Set { return ensureImageExists(u.images, command.SocialImageID.Value) } return nil } func overrideText(current string, override *string) string { if override == nil { return current } return *override } func overrideImage(current *uint32, override article.Nullable[uint32]) *uint32 { if override.Set { return override.Value } return current } func (c CheckArticleSEOCommand) applyTo(saved article.Article) article.Article { saved.Body = overrideText(saved.Body, c.Body) saved.Excerpt = overrideText(saved.Excerpt, c.Excerpt) saved.SEOTitle = overrideText(saved.SEOTitle, c.SEOTitle) saved.SEODescription = overrideText(saved.SEODescription, c.SEODescription) saved.CoverImageID = overrideImage(saved.CoverImageID, c.CoverImageID) saved.CoverImageAlt = overrideText(saved.CoverImageAlt, c.CoverImageAlt) saved.SocialTitle = overrideText(saved.SocialTitle, c.SocialTitle) saved.SocialDescription = overrideText(saved.SocialDescription, c.SocialDescription) saved.SocialImageID = overrideImage(saved.SocialImageID, c.SocialImageID) saved.CanonicalURL = overrideText(saved.CanonicalURL, c.CanonicalURL) if c.Noindex != nil { saved.Noindex = *c.Noindex } if c.SEODismissed != nil { saved.SEODismissed = article.NormalizeDismissed(*c.SEODismissed) } if c.Slug != nil { saved.Slug = *c.Slug if saved.Slug == "" { saved.Slug = article.GenerateSlug(saved.Title) } } return saved } func (u *CheckArticleSEOUseCase) checkSlug(candidate article.Article) (article.SEOSlugCheck, error) { available := article.SEOSlugCheck{Available: true} if candidate.Slug == "" { return available, nil } taken, err := u.articleRepository.SlugTaken(candidate.SiteID, candidate.Slug, candidate.ID) if err != nil || !taken { return available, err } suggestion, err := freeSlug(u.articleRepository, candidate.SiteID, candidate.Slug, candidate.ID, firstSuggestedAttempt) if err != nil { return article.SEOSlugCheck{}, err } return article.SEOSlugCheck{Suggestion: &suggestion}, nil }