feat: SEO backend output (2/4)
This commit is contained in:
1 parent
0eb46b0d74
commit
eb9876fde3
32 files changed
+3859
-65
No files matched your search
@@ -0,0 +1,127 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user