Files
RenewCMS/internal/infrastructure/useCases/CheckArticleSEO.go
T

131 lines
4.0 KiB
Go

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
}