From 7df94069908b6cebc7e3bc6a36af60081f360ec6 Mon Sep 17 00:00:00 2001 From: Stysusss <158248053+stysus@users.noreply.github.com> Date: Mon, 21 Sep 2026 03:55:28 +0700 Subject: [PATCH] fix(seo): normalize meta descriptions to 160 chars and fix homepage head tag - Add cleanMetaDescription in backend and frontend to normalize whitespace, collapse newlines, and truncate cleanly at 160-char word boundaries - Add explicit meta description and sync OpenGraph tags on homepage (+page.svelte) - Ensure single article detail and server injection output concise, single-line descriptions - Add unit test suite TestCleanMetaDescription in api handlers --- backend/internal/api/handlers_test.go | 61 +++++++++++++++++++++++++ backend/internal/api/server.go | 41 +++++++++++++++-- frontend/src/lib/index.ts | 1 + frontend/src/lib/seo.ts | 22 +++++++++ frontend/src/routes/+page.svelte | 13 ++++-- frontend/src/routes/[slug]/+page.svelte | 10 ++-- 6 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 frontend/src/lib/seo.ts diff --git a/backend/internal/api/handlers_test.go b/backend/internal/api/handlers_test.go index 4ed58d1..5fa8b9e 100644 --- a/backend/internal/api/handlers_test.go +++ b/backend/internal/api/handlers_test.go @@ -2313,3 +2313,64 @@ func TestClusterAPIEndpoints(t *testing.T) { t.Errorf("id1 is_primary = true after promotion, want false") } } + +func TestCleanMetaDescription(t *testing.T) { + tests := []struct { + name string + input string + maxLen int + expected string + }{ + { + name: "empty input", + input: "", + maxLen: 160, + expected: "", + }, + { + name: "whitespace only", + input: " \n\t \r\n ", + maxLen: 160, + expected: "", + }, + { + name: "normalize whitespace without truncation when maxLen is 0", + input: "Line 1.\n\nLine 2 with \t multiple spaces.\r\nLine 3.", + maxLen: 0, + expected: "Line 1. Line 2 with multiple spaces. Line 3.", + }, + { + name: "short string untouched", + input: "Curated intelligence on frontier AI research.", + maxLen: 160, + expected: "Curated intelligence on frontier AI research.", + }, + { + name: "long summary truncated cleanly at word boundary", + input: "Co-Scientist, an AI tool developed by Google researchers, is being used to accelerate aging research by generating novel genetic leads and rapidly analyzing complex screening data. It proposed over 20 plausible genetic factors for reversing cellular senescence, two of which were validated in lab tests, and reduced a six-month data interpretation process to just days.\n\nFor the tech and biotech industries, this demonstrates how AI agents can slash research timelines by automating literature synthesis and hypothesis generation, enabling faster translation of complex biological data into actionable experiments.", + maxLen: 160, + expected: "Co-Scientist, an AI tool developed by Google researchers, is being used to accelerate aging research by generating novel genetic leads and rapidly analyzing...", + }, + { + name: "trailing punctuation trimmed before ellipsis", + input: "Alpha, Beta, Gamma, Delta, Epsilon, Zeta, Eta, Theta, Iota, Kappa, Lambda, Mu, Nu, Xi, Omicron, Pi, Rho, Sigma, Tau, Upsilon, Phi, Chi, Psi, Omega - All in order.", + maxLen: 60, + expected: "Alpha, Beta, Gamma, Delta, Epsilon, Zeta, Eta, Theta...", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := cleanMetaDescription(tc.input, tc.maxLen) + if got != tc.expected { + t.Errorf("cleanMetaDescription() = %q, want %q", got, tc.expected) + } + if tc.maxLen > 0 && len([]rune(got)) > tc.maxLen { + t.Errorf("len(got) = %d > maxLen %d", len([]rune(got)), tc.maxLen) + } + if strings.ContainsAny(got, "\r\n\t") { + t.Errorf("got contains raw newline or tab: %q", got) + } + }) + } +} diff --git a/backend/internal/api/server.go b/backend/internal/api/server.go index cd29498..e330fad 100644 --- a/backend/internal/api/server.go +++ b/backend/internal/api/server.go @@ -450,6 +450,36 @@ func calculateReadingTime(text string) string { return fmt.Sprintf("%d min read", minutes) } +// cleanMetaDescription normalizes whitespace (stripping newlines, tabs, and +// multiple spaces) and optionally truncates at a clean word boundary without +// exceeding maxLen characters, ending with an ellipsis. If maxLen <= 0, it +// performs whitespace normalization without truncation. +func cleanMetaDescription(text string, maxLen int) string { + fields := strings.Fields(strings.TrimSpace(text)) + if len(fields) == 0 { + return "" + } + cleaned := strings.Join(fields, " ") + runes := []rune(cleaned) + + if maxLen <= 0 || len(runes) <= maxLen { + return cleaned + } + + target := maxLen - 3 + if target <= 0 { + return string(runes[:maxLen]) + } + + sub := string(runes[:target]) + lastSpace := strings.LastIndex(sub, " ") + if lastSpace > 0 { + sub = sub[:lastSpace] + } + sub = strings.TrimRight(sub, " ,;:.-–—") + return sub + "..." +} + func injectSEOTags(content []byte, pageTitle, desc, pageURL, imageURL, ogType string) []byte { if pageTitle != "" { content = titleTagRegex.ReplaceAll(content, []byte("