Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions backend/internal/api/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
41 changes: 36 additions & 5 deletions backend/internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,16 +450,47 @@ 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("<title>"+html.EscapeString(pageTitle)+"</title>"))
content = metaOgTitleRegex.ReplaceAll(content, []byte(`<meta property="og:title" content="`+html.EscapeString(pageTitle)+`" />`))
content = metaTwTitleRegex.ReplaceAll(content, []byte(`<meta name="twitter:title" content="`+html.EscapeString(pageTitle)+`" />`))
}
if desc != "" {
content = metaDescRegex.ReplaceAll(content, []byte(`<meta name="description" content="`+html.EscapeString(desc)+`" />`))
content = metaOgDescRegex.ReplaceAll(content, []byte(`<meta property="og:description" content="`+html.EscapeString(desc)+`" />`))
content = metaTwDescRegex.ReplaceAll(content, []byte(`<meta name="twitter:description" content="`+html.EscapeString(desc)+`" />`))
cleanDesc := cleanMetaDescription(desc, 0)
content = metaDescRegex.ReplaceAll(content, []byte(`<meta name="description" content="`+html.EscapeString(cleanDesc)+`" />`))
content = metaOgDescRegex.ReplaceAll(content, []byte(`<meta property="og:description" content="`+html.EscapeString(cleanDesc)+`" />`))
content = metaTwDescRegex.ReplaceAll(content, []byte(`<meta name="twitter:description" content="`+html.EscapeString(cleanDesc)+`" />`))
}
if pageURL != "" {
content = metaOgURLRegex.ReplaceAll(content, []byte(`<meta property="og:url" content="`+html.EscapeString(pageURL)+`" />`))
Expand Down Expand Up @@ -556,9 +587,9 @@ func (s *Server) serveIndexHTML(w http.ResponseWriter, r *http.Request) {
preloadImage = article.ImageURL
}
articleTitle := fmt.Sprintf("%s | NeuralWire", article.Title)
articleDesc := article.Summary
articleDesc := cleanMetaDescription(article.Summary, 160)
if articleDesc == "" {
articleDesc = article.Title
articleDesc = cleanMetaDescription(article.Title, 160)
}
pageURL := fmt.Sprintf("https://neuralwire.info/%s", article.Slug)
readTime := calculateReadingTime(article.Summary)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './bookmarks.svelte';
export * from './mockData';
export * from './api';
export * from './seo';
22 changes: 22 additions & 0 deletions frontend/src/lib/seo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Normalizes whitespace (collapsing newlines, tabs, and multiple spaces into a single space)
* and optionally truncates cleanly at word boundary without exceeding maxLen characters.
*/
export function cleanMetaDescription(text: string, maxLen = 160): string {
if (!text) return '';
const cleaned = text.trim().replace(/\s+/g, ' ');
if (maxLen <= 0 || cleaned.length <= maxLen) {
return cleaned;
}
const target = maxLen - 3;
if (target <= 0) {
return cleaned.slice(0, maxLen);
}
let sub = cleaned.slice(0, target);
const lastSpace = sub.lastIndexOf(' ');
if (lastSpace > 0) {
sub = sub.slice(0, lastSpace);
}
sub = sub.replace(/[ ,;:.\-–—]+$/, '');
return `${sub}...`;
}
13 changes: 9 additions & 4 deletions frontend/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,27 @@
</script>

<svelte:head>
<title>NeuralWire | AI News, Neural Networks & Future Computation</title>
<meta
name="description"
content="Curated intelligence on frontier AI research, neural networks, machine learning, and computational industry."
/>
<link rel="canonical" href="{getSiteUrl()}/" />
<meta property="og:title" content="NeuralWire | AI News & Editorial" />
<meta property="og:title" content="NeuralWire | AI News, Neural Networks & Future Computation" />
<meta
property="og:description"
content="An editorial news portal for artificial intelligence, neural networks, and the future of computation."
content="Curated intelligence on frontier AI research, neural networks, machine learning, and computational industry."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="{getSiteUrl()}/" />
<meta property="og:image" content={homeOgImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="NeuralWire | AI News & Editorial" />
<meta name="twitter:title" content="NeuralWire | AI News, Neural Networks & Future Computation" />
<meta
name="twitter:description"
content="An editorial news portal for artificial intelligence, neural networks, and the future of computation."
content="Curated intelligence on frontier AI research, neural networks, machine learning, and computational industry."
/>
<meta name="twitter:image" content={homeOgImage} />
</svelte:head>
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/routes/[slug]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { absoluteUrl, getSiteUrl } from '$lib/siteUrl';
import { BASE_URL } from '$lib/api';
import { getOgImageUrl } from '$lib/og';
import { cleanMetaDescription } from '$lib/seo';

let { data }: { data: PageData } = $props();

Expand Down Expand Up @@ -65,13 +66,16 @@
// site as an Organization (web curator), avoiding fictional authors. `</`
// is escaped so a value containing the closing tag sequence cannot break
// out of the tag.
const metaDescription = $derived(cleanMetaDescription(article.summary || article.title, 160));

const articleJsonLdHtml = $derived(
'<scr' +
'ipt type="application/ld+json">' +
JSON.stringify({
'@context': 'https://schema.org',
'@type': 'NewsArticle',
headline: article.title,
description: metaDescription,
image: [absoluteUrl(article.image_url)],
datePublished: article.published_at || article.created_at,
dateModified: article.published_at || article.created_at,
Expand Down Expand Up @@ -288,12 +292,12 @@

<svelte:head>
<title>{article.title} | NeuralWire</title>
<meta name="description" content={article.summary} />
<meta name="description" content={metaDescription} />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="{getSiteUrl()}/{article.slug}" />
<!-- Article Specific OG (Branded Social Card First) -->
<meta property="og:title" content={article.title} />
<meta property="og:description" content={article.summary} />
<meta property="og:description" content={metaDescription} />
<meta property="og:type" content="article" />
<meta property="og:url" content="{getSiteUrl()}/{article.slug}" />
<meta property="og:image" content={socialOgImageUrl} />
Expand All @@ -302,7 +306,7 @@
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={article.title} />
<meta name="twitter:description" content={article.summary} />
<meta name="twitter:description" content={metaDescription} />
<meta name="twitter:image" content={socialOgImageUrl} />
<!-- Structured data: NewsArticle -->
{@html articleJsonLdHtml}
Expand Down
Loading