feat: SEO backend data (1/4)
This commit is contained in:
1 parent
cb1410f0b0
commit
0eb46b0d74
90 files changed
+6094
-350
No files matched your search
@@ -326,7 +326,7 @@ func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.ArticleRepo, renderer, recorder, repos.SiteRepo, repos.AuthorRepo),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo, renderer),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo, renderer, repos.SiteRepo),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo, renderer, recorder, repos.SiteRepo, repos.AuthorRepo),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo, renderer, recorder, repos.SiteRepo, repos.AuthorRepo, repos.ImageRepo),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo, recorder),
|
||||
ListAuthorsUseCase: useCases.NewListAuthorsUseCase(repos.AuthorRepo, repos.UserRepo),
|
||||
CreateAuthorUseCase: useCases.NewCreateAuthorUseCase(repos.AuthorRepo, repos.UserRepo, repos.ImageRepo, recorder),
|
||||
@@ -359,9 +359,9 @@ func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings
|
||||
OpenMediaUseCase: useCases.NewOpenMediaUseCase(imageStorage),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(repos.ImageRepo, repos.ArticleRepo, repos.UserRepo, repos.VariantFileRepo, repos.SiteRepo, repos.AuthorRepo),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(repos.ImageRepo, recorder),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(repos.ImageRepo, imageStorage, repos.VariantFileRepo, recorder, repos.SiteRepo, repos.AuthorRepo),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(repos.ImageRepo, imageStorage, repos.VariantFileRepo, recorder, repos.SiteRepo, repos.AuthorRepo, repos.ArticleRepo),
|
||||
MergeMediaUseCase: mergeMedia,
|
||||
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(repos.ImageRepo, repos.ArticleRepo),
|
||||
GetMediaUsageUseCase: useCases.NewGetMediaUsageUseCase(repos.ImageRepo, repos.ArticleRepo, repos.SiteRepo, repos.AuthorRepo),
|
||||
GetPurgeSettingsUseCase: useCases.NewGetPurgeSettingsUseCase(repos.PurgeSettingsRepo),
|
||||
UpdatePurgeSettingsUseCase: useCases.NewUpdatePurgeSettingsUseCase(repos.PurgeSettingsRepo, time.Now),
|
||||
PreviewPurgeUseCase: useCases.NewPreviewPurgeUseCase(repos.ImageRepo, repos.ArticleRepo, repos.SiteRepo, repos.AuthorRepo),
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
domain "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
playground "github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
@@ -47,7 +49,7 @@ func NewHandler(create *useCases.CreateArticleUseCase, get *useCases.GetArticleU
|
||||
}
|
||||
}
|
||||
|
||||
type articleResponse struct {
|
||||
type baseArticleResponse struct {
|
||||
ID uint32 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
@@ -59,12 +61,34 @@ type articleResponse struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func toArticleResponse(article domain.Article, format domain.BodyFormat) (articleResponse, error) {
|
||||
type articleResponse struct {
|
||||
baseArticleResponse
|
||||
Slug string `json:"slug"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
SEOTitle string `json:"seo_title"`
|
||||
SEODescription string `json:"seo_description"`
|
||||
CoverImageID *uint32 `json:"cover_image_id"`
|
||||
CoverImageAlt string `json:"cover_image_alt"`
|
||||
SocialTitle string `json:"social_title"`
|
||||
SocialDescription string `json:"social_description"`
|
||||
SocialImageID *uint32 `json:"social_image_id"`
|
||||
CanonicalURL string `json:"canonical_url"`
|
||||
Noindex bool `json:"noindex"`
|
||||
SEODismissed []string `json:"seo_dismissed"`
|
||||
}
|
||||
|
||||
type listItemResponse struct {
|
||||
baseArticleResponse
|
||||
Excerpt string `json:"excerpt"`
|
||||
Slug string `json:"slug"`
|
||||
}
|
||||
|
||||
func toBaseResponse(article domain.Article, format domain.BodyFormat) (baseArticleResponse, error) {
|
||||
body := json.RawMessage(article.Body)
|
||||
if format != domain.BodyFormatJSON {
|
||||
encoded, err := json.Marshal(article.Body)
|
||||
if err != nil {
|
||||
return articleResponse{}, err
|
||||
return baseArticleResponse{}, err
|
||||
}
|
||||
body = encoded
|
||||
}
|
||||
@@ -72,7 +96,7 @@ func toArticleResponse(article domain.Article, format domain.BodyFormat) (articl
|
||||
if authors == nil {
|
||||
authors = []author.Byline{}
|
||||
}
|
||||
return articleResponse{
|
||||
return baseArticleResponse{
|
||||
ID: article.ID,
|
||||
Title: article.Title,
|
||||
Body: body,
|
||||
@@ -85,6 +109,40 @@ func toArticleResponse(article domain.Article, format domain.BodyFormat) (articl
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toArticleResponse(article domain.Article, format domain.BodyFormat) (articleResponse, error) {
|
||||
base, err := toBaseResponse(article, format)
|
||||
if err != nil {
|
||||
return articleResponse{}, err
|
||||
}
|
||||
dismissed := article.SEODismissed
|
||||
if dismissed == nil {
|
||||
dismissed = []string{}
|
||||
}
|
||||
return articleResponse{
|
||||
baseArticleResponse: base,
|
||||
Slug: article.Slug,
|
||||
Excerpt: article.Excerpt,
|
||||
SEOTitle: article.SEOTitle,
|
||||
SEODescription: article.SEODescription,
|
||||
CoverImageID: article.CoverImageID,
|
||||
CoverImageAlt: article.CoverImageAlt,
|
||||
SocialTitle: article.SocialTitle,
|
||||
SocialDescription: article.SocialDescription,
|
||||
SocialImageID: article.SocialImageID,
|
||||
CanonicalURL: article.CanonicalURL,
|
||||
Noindex: article.Noindex,
|
||||
SEODismissed: dismissed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toListItemResponse(article domain.Article, format domain.BodyFormat) (listItemResponse, error) {
|
||||
base, err := toBaseResponse(article, format)
|
||||
if err != nil {
|
||||
return listItemResponse{}, err
|
||||
}
|
||||
return listItemResponse{baseArticleResponse: base, Excerpt: article.Excerpt, Slug: article.Slug}, nil
|
||||
}
|
||||
|
||||
func encodeArticle(article domain.Article, format domain.BodyFormat) ([]byte, error) {
|
||||
response, err := toArticleResponse(article, format)
|
||||
if err != nil {
|
||||
@@ -174,7 +232,9 @@ func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, apperr.ArticleTitleErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
http.Error(w, "Failed to create article", http.StatusInternalServerError)
|
||||
if !writeKnownArticleError(w, err) {
|
||||
http.Error(w, "Failed to create article", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -182,19 +242,62 @@ func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func writeArticleErrorResponse(w http.ResponseWriter, err error) {
|
||||
if !writeKnownArticleError(w, err) {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func writeKnownArticleError(w http.ResponseWriter, err error) bool {
|
||||
if errors.Is(err, domain.ErrArticleNotFound) {
|
||||
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
|
||||
return
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, site.ErrSiteNotFound) {
|
||||
http.Error(w, apperr.SiteNotFoundErrorMessage, http.StatusNotFound)
|
||||
return
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, author.ErrAuthorNotFound) {
|
||||
http.Error(w, apperr.AuthorNotFoundErrorMessage, http.StatusNotFound)
|
||||
return
|
||||
return true
|
||||
}
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
if errors.Is(err, domain.ErrSlugTaken) {
|
||||
writeSlugTaken(w, err)
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, domain.ErrInvalidSlug) {
|
||||
http.Error(w, apperr.InvalidSlugErrorMessage, http.StatusBadRequest)
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, domain.ErrInvalidCanonicalURL) {
|
||||
http.Error(w, apperr.InvalidURLErrorMessage, http.StatusUnprocessableEntity)
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, image.ErrImageNotFound) {
|
||||
http.Error(w, apperr.ArticleImageNotFoundErrorMessage, http.StatusBadRequest)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func writeSlugTaken(w http.ResponseWriter, err error) {
|
||||
var conflict *domain.SlugTakenError
|
||||
if errors.As(err, &conflict) && conflict.Suggestion != "" {
|
||||
w.Header().Set(apperr.SlugSuggestionHeader, conflict.Suggestion)
|
||||
}
|
||||
http.Error(w, apperr.ArticleSlugTakenErrorMessage, http.StatusConflict)
|
||||
}
|
||||
|
||||
func writeValidationError(w http.ResponseWriter, err error) {
|
||||
var failures playground.ValidationErrors
|
||||
if errors.As(err, &failures) {
|
||||
for _, failure := range failures {
|
||||
if failure.Tag() == validator.ArticleSlugTag {
|
||||
http.Error(w, apperr.InvalidSlugErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func writeArticleJsonResponse(w http.ResponseWriter, status int, article domain.Article, format domain.BodyFormat) {
|
||||
@@ -233,13 +336,50 @@ func (h *Handler) setIsOnline(w http.ResponseWriter, r *http.Request, isOnline b
|
||||
writeArticleJsonResponse(w, http.StatusOK, updatedArticle, format)
|
||||
}
|
||||
|
||||
type UpdateArticleBody struct {
|
||||
Body *string `json:"body"`
|
||||
SiteID *uint32 `json:"site_id" validate:"omitempty,gte=1"`
|
||||
AuthorIDs *[]uint32 `json:"author_ids" validate:"omitempty,dive,gte=1"`
|
||||
type optionalImageID struct {
|
||||
Set bool
|
||||
Value *uint32 `validate:"omitempty,gte=1"`
|
||||
}
|
||||
|
||||
var updatableArticleFields = []string{"body", "site_id", "author_ids"}
|
||||
func (o *optionalImageID) UnmarshalJSON(data []byte) error {
|
||||
o.Set = true
|
||||
if string(data) == "null" {
|
||||
o.Value = nil
|
||||
return nil
|
||||
}
|
||||
var value uint32
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return err
|
||||
}
|
||||
o.Value = &value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o optionalImageID) toNullable() domain.Nullable[uint32] {
|
||||
return domain.Nullable[uint32]{Set: o.Set, Value: o.Value}
|
||||
}
|
||||
|
||||
type UpdateArticleBody struct {
|
||||
Body *string `json:"body"`
|
||||
SiteID *uint32 `json:"site_id" validate:"omitempty,gte=1"`
|
||||
AuthorIDs *[]uint32 `json:"author_ids" validate:"omitempty,dive,gte=1"`
|
||||
Slug *string `json:"slug" validate:"omitempty,articleslug"`
|
||||
Excerpt *string `json:"excerpt" validate:"omitempty,max=300"`
|
||||
SEOTitle *string `json:"seo_title" validate:"omitempty,max=120"`
|
||||
SEODescription *string `json:"seo_description" validate:"omitempty,max=320"`
|
||||
CoverImageID optionalImageID `json:"cover_image_id"`
|
||||
CoverImageAlt *string `json:"cover_image_alt" validate:"omitempty,max=200"`
|
||||
SocialTitle *string `json:"social_title" validate:"omitempty,max=120"`
|
||||
SocialDescription *string `json:"social_description" validate:"omitempty,max=320"`
|
||||
SocialImageID optionalImageID `json:"social_image_id"`
|
||||
CanonicalURL *string `json:"canonical_url" validate:"omitempty,max=2048"`
|
||||
Noindex *bool `json:"noindex"`
|
||||
SEODismissed *[]string `json:"seo_dismissed" validate:"omitempty,dive,oneof=title_long title_empty desc_short desc_long cover_alt cover_none slug_long no_au no_h body_alt short"`
|
||||
}
|
||||
|
||||
var nonNullableArticleFields = []string{"body", "site_id", "author_ids", "slug", "excerpt", "seo_title", "seo_description", "cover_image_alt", "social_title", "social_description", "canonical_url", "noindex", "seo_dismissed"}
|
||||
|
||||
var nullableArticleFields = []string{"cover_image_id", "social_image_id"}
|
||||
|
||||
func hasDuplicates(ids []uint32) bool {
|
||||
seen := make(map[uint32]bool, len(ids))
|
||||
@@ -262,7 +402,7 @@ func readUpdateArticleBody(r *http.Request) (UpdateArticleBody, bool) {
|
||||
return UpdateArticleBody{}, false
|
||||
}
|
||||
known := 0
|
||||
for _, name := range updatableArticleFields {
|
||||
for _, name := range nonNullableArticleFields {
|
||||
value, present := fields[name]
|
||||
if !present {
|
||||
continue
|
||||
@@ -272,6 +412,11 @@ func readUpdateArticleBody(r *http.Request) (UpdateArticleBody, bool) {
|
||||
}
|
||||
known++
|
||||
}
|
||||
for _, name := range nullableArticleFields {
|
||||
if _, present := fields[name]; present {
|
||||
known++
|
||||
}
|
||||
}
|
||||
if known == 0 {
|
||||
return UpdateArticleBody{}, false
|
||||
}
|
||||
@@ -304,15 +449,27 @@ func (h *Handler) UpdateArticleBody(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err := validator.Validate.Struct(request); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
writeValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
updatedArticle, err := h.UpdateUseCase.Update(r.Context(), id, useCases.UpdateArticleCommand{
|
||||
Body: request.Body,
|
||||
SiteID: request.SiteID,
|
||||
AuthorIDs: request.AuthorIDs,
|
||||
Format: format,
|
||||
Body: request.Body,
|
||||
SiteID: request.SiteID,
|
||||
AuthorIDs: request.AuthorIDs,
|
||||
Slug: request.Slug,
|
||||
Excerpt: request.Excerpt,
|
||||
SEOTitle: request.SEOTitle,
|
||||
SEODescription: request.SEODescription,
|
||||
CoverImageID: request.CoverImageID.toNullable(),
|
||||
CoverImageAlt: request.CoverImageAlt,
|
||||
SocialTitle: request.SocialTitle,
|
||||
SocialDescription: request.SocialDescription,
|
||||
SocialImageID: request.SocialImageID.toNullable(),
|
||||
CanonicalURL: request.CanonicalURL,
|
||||
Noindex: request.Noindex,
|
||||
SEODismissed: request.SEODismissed,
|
||||
Format: format,
|
||||
})
|
||||
if err != nil {
|
||||
writeArticleErrorResponse(w, err)
|
||||
@@ -366,11 +523,11 @@ func parseSort(value string) domain.Sort {
|
||||
}
|
||||
|
||||
type listResponse struct {
|
||||
Items []articleResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Counts domain.Counts `json:"counts"`
|
||||
Items []listItemResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Counts domain.Counts `json:"counts"`
|
||||
}
|
||||
|
||||
func intQueryParam(values url.Values, name string, fallback int) (int, error) {
|
||||
@@ -435,9 +592,9 @@ func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
responses := make([]articleResponse, 0, len(page.Items))
|
||||
responses := make([]listItemResponse, 0, len(page.Items))
|
||||
for _, article := range page.Items {
|
||||
response, err := toArticleResponse(article, format)
|
||||
response, err := toListItemResponse(article, format)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -90,9 +90,11 @@ func intQueryParam(values url.Values, name string, fallback int) (int, error) {
|
||||
}
|
||||
|
||||
type usageResponse struct {
|
||||
ID uint32 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Published bool `json:"published"`
|
||||
Type string `json:"type"`
|
||||
ID uint32 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Published *bool `json:"published,omitempty"`
|
||||
Fields []string `json:"fields"`
|
||||
}
|
||||
|
||||
type mergeResponse struct {
|
||||
@@ -324,15 +326,15 @@ func (h *Handler) Usage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
articles, err := h.UsageUseCase.Usage(id)
|
||||
usages, err := h.UsageUseCase.Usage(id)
|
||||
if err != nil {
|
||||
writeImageError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response := make([]usageResponse, 0, len(articles))
|
||||
for _, a := range articles {
|
||||
response = append(response, usageResponse{ID: a.ID, Title: a.Title, Published: a.IsOnline})
|
||||
response := make([]usageResponse, 0, len(usages))
|
||||
for _, usage := range usages {
|
||||
response = append(response, usageResponse{Type: usage.Type, ID: usage.ID, Title: usage.Title, Published: usage.Published, Fields: usage.Fields})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ type PostSite struct {
|
||||
Locale *string `json:"locale"`
|
||||
DefaultSocialImageID *uint32 `json:"default_social_image_id"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
SiteName string `json:"site_name" validate:"max=100"`
|
||||
TitleTemplate *string `json:"title_template" validate:"omitempty,max=200"`
|
||||
DefaultDescription string `json:"default_description" validate:"max=320"`
|
||||
}
|
||||
|
||||
type optional[T any] struct {
|
||||
@@ -64,6 +67,9 @@ type PatchSite struct {
|
||||
Locale optional[string] `json:"locale"`
|
||||
DefaultSocialImageID optional[uint32] `json:"default_social_image_id"`
|
||||
IsDefault *bool `json:"is_default"`
|
||||
SiteName *string `json:"site_name" validate:"omitempty,max=100"`
|
||||
TitleTemplate *string `json:"title_template" validate:"omitempty,max=200"`
|
||||
DefaultDescription *string `json:"default_description" validate:"omitempty,max=320"`
|
||||
}
|
||||
|
||||
func (p PatchSite) toPatch() domain.Patch {
|
||||
@@ -75,6 +81,9 @@ func (p PatchSite) toPatch() domain.Patch {
|
||||
Locale: domain.Nullable[string]{Set: p.Locale.Set, Value: p.Locale.Value},
|
||||
DefaultSocialImageID: domain.Nullable[uint32]{Set: p.DefaultSocialImageID.Set, Value: p.DefaultSocialImageID.Value},
|
||||
IsDefault: p.IsDefault,
|
||||
SiteName: p.SiteName,
|
||||
TitleTemplate: p.TitleTemplate,
|
||||
DefaultDescription: p.DefaultDescription,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +120,8 @@ func writeSiteError(w http.ResponseWriter, err error) {
|
||||
http.Error(w, apperr.SiteKeyImmutableErrorMessage, http.StatusBadRequest)
|
||||
case errors.Is(err, domain.ErrInvalidSiteURL):
|
||||
http.Error(w, apperr.InvalidSiteURLErrorMessage, http.StatusBadRequest)
|
||||
case errors.Is(err, domain.ErrInvalidTitleTemplate):
|
||||
http.Error(w, apperr.InvalidTitleTemplateErrorMessage, http.StatusBadRequest)
|
||||
case errors.Is(err, domain.ErrInvalidLocale):
|
||||
http.Error(w, apperr.InvalidLocaleErrorMessage, http.StatusBadRequest)
|
||||
case errors.Is(err, image.ErrImageNotFound):
|
||||
@@ -159,6 +170,9 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
Locale: request.Locale,
|
||||
DefaultSocialImageID: request.DefaultSocialImageID,
|
||||
IsDefault: request.IsDefault,
|
||||
SiteName: request.SiteName,
|
||||
TitleTemplate: request.TitleTemplate,
|
||||
DefaultDescription: request.DefaultDescription,
|
||||
})
|
||||
if err != nil {
|
||||
writeSiteError(w, err)
|
||||
|
||||
@@ -81,3 +81,8 @@ const InvalidURLErrorMessage = "invalid_url"
|
||||
const TooManyLinksErrorMessage = "too_many_links"
|
||||
const InvalidSlugErrorMessage = "invalid_slug"
|
||||
const AuthorImageNotFoundErrorMessage = "image_not_found"
|
||||
|
||||
const ArticleSlugTakenErrorMessage = "slug_taken"
|
||||
const ArticleImageNotFoundErrorMessage = "image_not_found"
|
||||
const InvalidTitleTemplateErrorMessage = "invalid_title_template"
|
||||
const SlugSuggestionHeader = "X-Slug-Suggestion"
|
||||
@@ -1,6 +1,7 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/author"
|
||||
"RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/domain/site"
|
||||
@@ -17,6 +18,7 @@ const siteNameTag = "sitename"
|
||||
const AuthorNameTag = "authorname"
|
||||
const AuthorSlugTag = "authorslug"
|
||||
const SocialTypeTag = "socialtype"
|
||||
const ArticleSlugTag = "articleslug"
|
||||
|
||||
var inviteEmailPattern = regexp.MustCompile(`^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$`)
|
||||
|
||||
@@ -31,6 +33,15 @@ func InitValidator() {
|
||||
_ = Validate.RegisterValidation(AuthorNameTag, isAuthorName, true)
|
||||
_ = Validate.RegisterValidation(AuthorSlugTag, isAuthorSlug, true)
|
||||
_ = Validate.RegisterValidation(SocialTypeTag, isSocialType, true)
|
||||
_ = Validate.RegisterValidation(ArticleSlugTag, isArticleSlug, true)
|
||||
}
|
||||
|
||||
func isArticleSlug(field validator.FieldLevel) bool {
|
||||
if isAbsent(field) {
|
||||
return true
|
||||
}
|
||||
value := field.Field().String()
|
||||
return value == "" || article.IsValidSlug(value)
|
||||
}
|
||||
|
||||
func isAbsent(field validator.FieldLevel) bool {
|
||||
|
||||
Reference in new issue
Block a user