Files
RenewCMS/api/handlers/article/seoHandler.go
T

128 lines
4.4 KiB
Go

package article
import (
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/infrastructure/useCases"
"bytes"
"encoding/json"
"io"
"log"
"net/http"
)
type SEOHandler struct {
ResolveUseCase *useCases.ResolveArticleSEOUseCase
CheckUseCase *useCases.CheckArticleSEOUseCase
GetUserUseCase *useCases.GetUserUseCase
}
func NewSEOHandler(resolve *useCases.ResolveArticleSEOUseCase, check *useCases.CheckArticleSEOUseCase, getUser *useCases.GetUserUseCase) *SEOHandler {
return &SEOHandler{ResolveUseCase: resolve, CheckUseCase: check, GetUserUseCase: getUser}
}
type CheckArticleSEOBody struct {
Body *string `json:"body"`
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 checkNonNullableFields = []string{"body", "slug", "excerpt", "seo_title", "seo_description", "cover_image_alt", "social_title", "social_description", "canonical_url", "noindex", "seo_dismissed"}
func readCheckBody(r *http.Request) (CheckArticleSEOBody, bool) {
raw, err := io.ReadAll(r.Body)
if err != nil {
return CheckArticleSEOBody{}, false
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil || fields == nil {
return CheckArticleSEOBody{}, false
}
known, ok := countKnownFields(fields, checkNonNullableFields)
if !ok || known != len(fields) {
return CheckArticleSEOBody{}, false
}
var request CheckArticleSEOBody
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
return CheckArticleSEOBody{}, false
}
return request, true
}
func (h *SEOHandler) Resolve(w http.ResponseWriter, r *http.Request) {
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
resolved, err := h.ResolveUseCase.Resolve(id, isVerifiedCaller(r, h.GetUserUseCase))
if err != nil {
writeArticleErrorResponse(w, err)
return
}
writeSEOJson(w, resolved)
}
func (h *SEOHandler) Check(w http.ResponseWriter, r *http.Request) {
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
request, ok := readCheckBody(r)
if !ok {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
if err := validator.Validate.Struct(request); err != nil {
writeValidationError(w, err)
return
}
checked, err := h.CheckUseCase.Check(id, useCases.CheckArticleSEOCommand{
Body: request.Body,
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,
})
if err != nil {
writeArticleErrorResponse(w, err)
return
}
writeSEOJson(w, checked)
}
func writeSEOJson(w http.ResponseWriter, payload any) {
encoded, err := json.Marshal(payload)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if _, err := w.Write(encoded); err != nil {
log.Printf("Failed to write response: %v", err)
}
}