fixes
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Meta title/description caps mirror seo.MetaTitleMaxChars / MetaDescriptionMaxChars.
|
||||
// Duplicated here so processing can fill meta without importing seo (seo imports processing).
|
||||
const (
|
||||
metaTitleMaxChars = 60
|
||||
metaDescriptionMaxChars = 155
|
||||
)
|
||||
|
||||
// Bare "| <digits>" suffix = legacy unique_id leak (e.g. "VOX EBR700 | 50").
|
||||
// Matches upsertProcessedProductSQL refresh predicate.
|
||||
var poisonedMetaTitleUniqueIDRe = regexp.MustCompile(`\|\s+[0-9]+$`)
|
||||
|
||||
// isPoisonedMetaTitle reports meta_title that must be refreshed: bare unique_id
|
||||
// suffix ("… | 50") or enhance-prompt instruction leakage ("short retail title",
|
||||
// "Title formula", …). Matches upsert / BackfillMissingMeta SQL predicates.
|
||||
func isPoisonedMetaTitle(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
if poisonedMetaTitleUniqueIDRe.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
return isPromptLeakageTitle(s)
|
||||
}
|
||||
|
||||
// fillMetaFromResult builds free template meta from a StepResult (FillMetaTemplate-equivalent).
|
||||
// Does not call AI / does not spend credits.
|
||||
func fillMetaFromResult(result StepResult) (title, description string) {
|
||||
name := strings.TrimSpace(result.ProcessedName)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(result.Name)
|
||||
}
|
||||
if name == "" {
|
||||
name = "Product"
|
||||
}
|
||||
cat := categoryDisplayLabel(result)
|
||||
brand := metaBrandFromAttrs(result.ProcessedAttributes, result.Attributes)
|
||||
|
||||
parts := make([]string, 0, 3)
|
||||
if brand != "" && !strings.Contains(strings.ToLower(name), strings.ToLower(brand)) {
|
||||
parts = append(parts, brand)
|
||||
}
|
||||
parts = append(parts, name)
|
||||
if cat != "" && !strings.Contains(strings.ToLower(name), strings.ToLower(cat)) {
|
||||
parts = append(parts, cat)
|
||||
}
|
||||
title = truncateMetaRunes(strings.Join(parts, " | "), metaTitleMaxChars)
|
||||
|
||||
body := strings.TrimSpace(result.ProcessedDescription)
|
||||
if body == "" {
|
||||
body = strings.TrimSpace(result.Description)
|
||||
}
|
||||
body = stripMetaTags(body)
|
||||
if body == "" {
|
||||
var bits []string
|
||||
if brand != "" {
|
||||
bits = append(bits, brand)
|
||||
}
|
||||
bits = append(bits, name)
|
||||
if cat != "" {
|
||||
bits = append(bits, "in "+cat)
|
||||
}
|
||||
body = strings.Join(bits, " ") + ". Shop quality products with clear specs and fast delivery."
|
||||
}
|
||||
description = truncateMetaDescription(body, metaDescriptionMaxChars)
|
||||
return title, description
|
||||
}
|
||||
|
||||
// truncateMetaDescription collapses whitespace and truncates at a word boundary when possible.
|
||||
func truncateMetaDescription(s string, max int) string {
|
||||
s = collapseMetaSpace(s)
|
||||
if max <= 0 {
|
||||
return ""
|
||||
}
|
||||
r := []rune(s)
|
||||
if len(r) <= max {
|
||||
return s
|
||||
}
|
||||
cut := r[:max]
|
||||
lastSpace := -1
|
||||
for i := len(cut) - 1; i >= 0; i-- {
|
||||
if unicode.IsSpace(cut[i]) {
|
||||
lastSpace = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastSpace > max/2 {
|
||||
return strings.TrimSpace(string(cut[:lastSpace]))
|
||||
}
|
||||
return string(cut)
|
||||
}
|
||||
|
||||
func truncateMetaRunes(s string, max int) string {
|
||||
s = collapseMetaSpace(s)
|
||||
r := []rune(s)
|
||||
if max <= 0 || len(r) <= max {
|
||||
return s
|
||||
}
|
||||
if max <= 3 {
|
||||
return string(r[:max])
|
||||
}
|
||||
return string(r[:max-1]) + "…"
|
||||
}
|
||||
|
||||
func collapseMetaSpace(s string) string {
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
||||
func stripMetaTags(s string) string {
|
||||
var b strings.Builder
|
||||
inTag := false
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r == '<':
|
||||
inTag = true
|
||||
case r == '>':
|
||||
inTag = false
|
||||
case !inTag:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func metaBrandFromAttrs(bags ...map[string]any) string {
|
||||
keys := []string{"brand", "Brand", "manufacturer"}
|
||||
for _, m := range bags {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
for _, k := range keys {
|
||||
if v, ok := m[k]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
if t := strings.TrimSpace(s); t != "" {
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user