From 601e448fa82cd85692fe053693913a076efe24f4 Mon Sep 17 00:00:00 2001
From: Oseltamivir <58582368+Oseltamivir@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:20:22 +0800
Subject: [PATCH 1/2] feat(seo): lead compare/blog titles with search phrase,
stat-led + trimmed meta descriptions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Improve SERP click-through on page-1 compare pages and blog posts.
- Compare
now leads with the GPU pair (the actual query, e.g.
"B200 vs B300: GLM-5 Inference Benchmark | InferenceX") via title.absolute,
bypassing the long root template; falls back "Inference Benchmark"→"Benchmark"
for long pairs. New compareModelSeoName (single natural version name) +
compareSeoTitle in compare-slug.ts.
- Compare meta description is now stat-led from interpolated head-to-head
numbers at the default interactivity target (tok/s/GPU + $/M-tok), ≤155 chars,
gracefully falling back to compact boilerplate for sparse-data pairs.
New compareMetaDescription / computeCompareStat in compare-ssr.ts.
- Blog uses the shorter "| InferenceX" suffix (title.absolute).
- Blog meta/OG/Twitter descriptions use blogDescription = seoDescription ??
smartTruncate(subtitle, 155) (word-boundary cut, no mid-sentence truncation).
Added optional seoDescription frontmatter to 9 long-subtitle posts.
- Mirrored every change on /zh (compareMetaDescriptionZh, zh titles, zh
seoDescription on the 9 translated posts).
- Unit tests for compareModelSeoName, compareSeoTitle, compareMetaDescription
(+ zh structural port), smartTruncate, and blogDescription.
中文:让 /compare 与博客页面的 SEO 元数据以搜索短语开头,提升谷歌搜索结果页点击率。
compare 的 改为以 GPU 对(真实搜索词,如“B200 vs B300: GLM-5 Inference
Benchmark | InferenceX”)开头,使用 title.absolute 绕过冗长的根模板,长组合时将
“Inference Benchmark”缩短为“Benchmark”;新增 compareModelSeoName(单一自然版本名)
与 compareSeoTitle。compare 的 meta 描述改为在默认交互性目标下由插值出的正面对比
数据(tok/s/GPU 与 $/M-tok)驱动,控制在 155 字符内,数据稀疏时优雅回退到精简样板
文案;新增 compareMetaDescription / computeCompareStat。博客 改用更短的
“| InferenceX”后缀。博客 meta/OG/Twitter 描述改用 blogDescription =
seoDescription ?? smartTruncate(subtitle, 155)(按词边界截断,避免句中截断),并为
9 篇长副标题文章新增 seoDescription frontmatter。所有改动均在 /zh 侧同步(新增
compareMetaDescriptionZh、中文标题、9 篇译文的中文 seoDescription)。补充相应单元测试。
---
...nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx | 1 +
...vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx | 1 +
...h200-int4-kimi-k2-vllm-perf-per-dollar.mdx | 1 +
...b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx | 1 +
...nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx | 1 +
...max-open-source-inference-benchmarking.mdx | 1 +
...deepseek-v4-pro-sglang-110x-in-26-days.mdx | 1 +
...x-glm5-fp8-sglang-40-cheaper-than-b200.mdx | 1 +
...i355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx | 1 +
...nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx | 1 +
...vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx | 1 +
...h200-int4-kimi-k2-vllm-perf-per-dollar.mdx | 1 +
...b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx | 1 +
...nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx | 1 +
...max-open-source-inference-benchmarking.mdx | 1 +
...deepseek-v4-pro-sglang-110x-in-26-days.mdx | 1 +
...x-glm5-fp8-sglang-40-cheaper-than-b200.mdx | 1 +
...i355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx | 1 +
packages/app/src/app/blog/[slug]/page.tsx | 13 +-
packages/app/src/app/compare/[slug]/page.tsx | 36 ++--
packages/app/src/app/zh/blog/[slug]/page.tsx | 19 +-
.../app/src/app/zh/compare/[slug]/page.tsx | 25 ++-
packages/app/src/lib/blog.test.ts | 56 ++++++
packages/app/src/lib/blog.ts | 36 ++++
packages/app/src/lib/compare-slug.test.ts | 65 +++++++
packages/app/src/lib/compare-slug.ts | 41 +++++
packages/app/src/lib/compare-ssr-zh.ts | 71 +++++++-
packages/app/src/lib/compare-ssr.test.ts | 170 +++++++++++++++++-
packages/app/src/lib/compare-ssr.ts | 146 +++++++++++++++
.../app/src/lib/compare-variant-ssr.test.ts | 1 +
30 files changed, 666 insertions(+), 31 deletions(-)
diff --git a/packages/app/content/blog/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx b/packages/app/content/blog/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx
index 217217f6..38592c9a 100644
--- a/packages/app/content/blog/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx
+++ b/packages/app/content/blog/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx
@@ -1,6 +1,7 @@
---
title: 'B200 NVFP4 vs H200 FP8 on GLM-5: Up to 3.65x Better Performance per Dollar with SGLang MTP'
subtitle: 'Both SKUs run SGLang EAGLE MTP; the Blackwell generation lifts perf/$ by ~1.2x at the peak and the NVIDIA GLM-5-NVFP4 checkpoint on FlashInfer TRT-LLM sparse MLA stacks another ~2.4–3.0x on 8K/1K'
+seoDescription: 'B200 NVFP4 delivers up to 3.65x better perf/$ than H200 FP8 on GLM-5 with SGLang MTP, stacking Blackwell gains and a sparse-MLA checkpoint.'
date: '2026-05-26'
publishDate: '2026-05-26'
tags:
diff --git a/packages/app/content/blog/b200-minimax-m2-5-vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx b/packages/app/content/blog/b200-minimax-m2-5-vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx
index e0e1f710..24b36501 100644
--- a/packages/app/content/blog/b200-minimax-m2-5-vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx
+++ b/packages/app/content/blog/b200-minimax-m2-5-vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx
@@ -1,6 +1,7 @@
---
title: 'B200 NVFP4 vs H100 FP8 on MiniMax-M2.5: Up to 8.2x Better Performance per Dollar with vLLM'
subtitle: 'vLLM PR #36307 unlocks the trtllm-gen FP8 MoE kernel for MiniMax on B200; combined with NVFP4, perf/$ scales from 4.0x at 22 tok/s/user to 8.2x at 110 on 8K/1K'
+seoDescription: 'B200 NVFP4 delivers up to 8.2x better perf/$ than H100 FP8 on MiniMax-M2.5 with vLLM — trtllm-gen FP8 MoE plus NVFP4 on 8K/1K.'
date: '2026-05-26'
publishDate: '2026-05-26'
tags:
diff --git a/packages/app/content/blog/b200-nvfp4-vs-h200-int4-kimi-k2-vllm-perf-per-dollar.mdx b/packages/app/content/blog/b200-nvfp4-vs-h200-int4-kimi-k2-vllm-perf-per-dollar.mdx
index 4fb1167c..538aa54f 100644
--- a/packages/app/content/blog/b200-nvfp4-vs-h200-int4-kimi-k2-vllm-perf-per-dollar.mdx
+++ b/packages/app/content/blog/b200-nvfp4-vs-h200-int4-kimi-k2-vllm-perf-per-dollar.mdx
@@ -1,6 +1,7 @@
---
title: 'B200 NVFP4 vs H200 INT4 on Kimi K2.5/K2.6: Up to 2.95x Better Performance per Dollar'
subtitle: "On vLLM 8K/1K the NVFP4 path on B200 is 2.71x–2.95x cheaper per million tokens than H200 INT4 across the entire 30–90 tok/s/user serving band, and 2.45x–2.74x cheaper than B200 INT4 on the same silicon. Both factors decompose cleanly into B200's HBM bandwidth, HBM capacity, and NVFP4 tensor cores"
+seoDescription: 'B200 NVFP4 runs Kimi K2.5/K2.6 up to 2.95x cheaper per token than H200 INT4 on vLLM 8K/1K, across the full 30–90 tok/s/user serving band.'
date: '2026-05-26'
publishDate: '2026-05-26'
tags:
diff --git a/packages/app/content/blog/gb200-nvl72-vs-b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx b/packages/app/content/blog/gb200-nvl72-vs-b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx
index a65b8a21..aa4bb2ff 100644
--- a/packages/app/content/blog/gb200-nvl72-vs-b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx
+++ b/packages/app/content/blog/gb200-nvl72-vs-b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx
@@ -1,6 +1,7 @@
---
title: 'GB200 NVL72 vs B200 on DeepSeek R1 670B: Up to 4.4x Throughput per GPU at 125 tok/s/user'
subtitle: "DeepSeek R1 FP4 1k/1k. NVL72's 72-GPU NVLink scale-up fabric lets decode run wide EP up to EP=32, where B200's 8-GPU NVLink island caps out at EP=8 over RoCEv2"
+seoDescription: 'GB200 NVL72 hits up to 4.4x more per-GPU throughput than B200 on DeepSeek R1 670B at 125 tok/s/user — 72-GPU NVLink enables wide EP=32.'
date: '2026-05-23'
publishDate: '2026-05-23'
tags:
diff --git a/packages/app/content/blog/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx b/packages/app/content/blog/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx
index 4f8fac9e..fd310844 100644
--- a/packages/app/content/blog/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx
+++ b/packages/app/content/blog/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx
@@ -1,6 +1,7 @@
---
title: 'GB300 NVL72 vs GB200 NVL72 Inference Performance & Perf per Dollar - on DeepSeek-V4-Pro 1.6T: Up to 2.83x Throughput'
subtitle: "DSv4-Pro FP4 8K/1K, Dynamo+vLLM, disaggregated on both racks. GB300's 50% extra HBM (288 vs 192 GB/GPU) unlocks a wider prefill+decode recipe GB200 can't fit — lifting middle-of-curve perf/$ by 2.31x despite a 20% per-GPU TCO premium."
+seoDescription: 'GB300 NVL72 adds 50% more HBM, lifting DeepSeek-V4-Pro perf/$ up to 2.31x over GB200 NVL72 on vLLM FP4 8K/1K — up to 2.83x throughput per GPU.'
date: '2026-05-27'
publishDate: '2026-05-27'
tags:
diff --git a/packages/app/content/blog/inferencemax-open-source-inference-benchmarking.mdx b/packages/app/content/blog/inferencemax-open-source-inference-benchmarking.mdx
index 4b6da177..32217ffa 100644
--- a/packages/app/content/blog/inferencemax-open-source-inference-benchmarking.mdx
+++ b/packages/app/content/blog/inferencemax-open-source-inference-benchmarking.mdx
@@ -1,6 +1,7 @@
---
title: 'InferenceMAX: Open Source Inference Benchmarking'
subtitle: 'NVIDIA GB200 NVL72, AMD MI355X, Throughput Token per GPU, Latency Tok/s/user, Perf per Dollar, Cost per Million Tokens, Tokens per Provisioned Megawatt, DeepSeek R1 670B, GPTOSS 120B, Llama3 70B'
+seoDescription: 'InferenceMAX: open-source inference benchmarks across NVIDIA GB200 NVL72 and AMD MI355X — throughput, latency, cost per token and perf per watt.'
date: '2025-10-09'
publishDate: '2025-10-09'
tags:
diff --git a/packages/app/content/blog/mi355x-deepseek-v4-pro-sglang-110x-in-26-days.mdx b/packages/app/content/blog/mi355x-deepseek-v4-pro-sglang-110x-in-26-days.mdx
index b4e9c850..ed001196 100644
--- a/packages/app/content/blog/mi355x-deepseek-v4-pro-sglang-110x-in-26-days.mdx
+++ b/packages/app/content/blog/mi355x-deepseek-v4-pro-sglang-110x-in-26-days.mdx
@@ -1,6 +1,7 @@
---
title: 'MI355X DeepSeek-V4-Pro on SGLang: 110.5x Throughput per GPU in 26 Days'
subtitle: 'The amd/deepseek_v4 side branch shipped TileLang attention indexer, Triton sparse MLA, fused RoPE/Hadamard, FlyDSL MoE, and FP4 weights across 31 performance optimizations PRs — lifting first-light 20 tok/s/GPU at 2.4 tok/s/user into 2,256 tok/s/GPU at 9.4 tok/s/user on 8K/1K, with both throughput and interactivity climbing together'
+seoDescription: 'MI355X ran DeepSeek-V4-Pro 110.5x faster per GPU in 26 days on SGLang — from 20 to 2,256 tok/s/GPU via TileLang, sparse MLA and FP4 on 8K/1K.'
date: '2026-05-26'
publishDate: '2026-05-26'
tags:
diff --git a/packages/app/content/blog/mi355x-glm5-fp8-sglang-40-cheaper-than-b200.mdx b/packages/app/content/blog/mi355x-glm5-fp8-sglang-40-cheaper-than-b200.mdx
index 353df6b4..b4212a6a 100644
--- a/packages/app/content/blog/mi355x-glm5-fp8-sglang-40-cheaper-than-b200.mdx
+++ b/packages/app/content/blog/mi355x-glm5-fp8-sglang-40-cheaper-than-b200.mdx
@@ -1,6 +1,7 @@
---
title: 'AMD MI355X GLM-5 Inference: Up to 40% Cheaper per Million Tokens than B200 on SGLang FP8'
subtitle: '14 weeks after GLM-5 launched, AMD landed both MTP and non-MTP SGLang FP8 recipes on MI355X — fused MLA + FP8 KV cache via TileLang flips the single-node FP8 cost curve in AMD favor across most of the performance Pareto'
+seoDescription: 'AMD MI355X runs GLM-5 up to 40% cheaper per million tokens than B200 on SGLang FP8, with fused MLA and FP8 KV cache via TileLang.'
date: '2026-05-25'
publishDate: '2026-05-25'
tags:
diff --git a/packages/app/content/blog/mi355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx b/packages/app/content/blog/mi355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx
index 329e4479..94983de0 100644
--- a/packages/app/content/blog/mi355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx
+++ b/packages/app/content/blog/mi355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx
@@ -1,6 +1,7 @@
---
title: 'AMD MI355X Qwen3.5 397B-A17B Inference: Up to 19x Throughput per GPU in 3 Months on SGLang FP8'
subtitle: 'From v0.5.8 (Feb) → v0.5.10rc0 (Apr) → v0.5.12 (May), three AITER kernel landings on MI355X plus a TP=8 → TP=2/TP=4 retune push Qwen3.5 8k/1k peak from 1.3k to 6.4k tok/s/GPU and extend the curve out to 75 tok/s/user'
+seoDescription: 'AMD MI355X lifted Qwen3.5 397B-A17B up to 19x per-GPU throughput in 3 months on SGLang FP8 — 1.3k to 6.4k tok/s/GPU via AITER kernels.'
date: '2026-05-25'
publishDate: '2026-05-25'
tags:
diff --git a/packages/app/content/blog/zh/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx b/packages/app/content/blog/zh/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx
index 36d30314..ec6db67a 100644
--- a/packages/app/content/blog/zh/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx
+++ b/packages/app/content/blog/zh/b200-glm5-nvfp4-vs-h200-fp8-3-6x-perf-per-dollar.mdx
@@ -1,6 +1,7 @@
---
title: 'B200 NVFP4 对比 H200 FP8 运行 GLM-5:SGLang MTP 下性价比提升高达 3.65 倍'
subtitle: '两款 GPU 均运行 SGLang EAGLE MTP;Blackwell 世代在峰值处带来约 1.2 倍的性价比提升,NVIDIA GLM-5-NVFP4 检查点搭配 FlashInfer TRT-LLM 稀疏 MLA 在 8K/1K 场景下再叠加约 2.4–3.0 倍优势'
+seoDescription: 'B200 NVFP4 在 SGLang MTP 下运行 GLM-5,性价比比 H200 FP8 最高提升 3.65 倍——叠加 Blackwell 世代增益与稀疏 MLA 检查点。'
date: '2026-05-26'
publishDate: '2026-05-26'
tags:
diff --git a/packages/app/content/blog/zh/b200-minimax-m2-5-vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx b/packages/app/content/blog/zh/b200-minimax-m2-5-vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx
index 73da4323..a889133c 100644
--- a/packages/app/content/blog/zh/b200-minimax-m2-5-vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx
+++ b/packages/app/content/blog/zh/b200-minimax-m2-5-vllm-nvfp4-vs-h100-fp8-perf-per-dollar.mdx
@@ -1,6 +1,7 @@
---
title: 'B200 NVFP4 vs H100 FP8 运行 MiniMax-M2.5:vLLM 下每美元性能最高提升 8.2 倍'
subtitle: 'vLLM PR #36307 为 MiniMax 在 B200 上解锁了 trtllm-gen FP8 MoE 模块化内核;结合 NVFP4,在 8K/1K 负载下性能/成本从 22 tok/s/user 时的 4.0 倍扩大到 110 tok/s/user 时的 8.2 倍'
+seoDescription: 'B200 NVFP4 在 vLLM 下运行 MiniMax-M2.5,每美元性能比 H100 FP8 最高提升 8.2 倍——trtllm-gen FP8 MoE 结合 NVFP4(8K/1K)。'
date: '2026-05-26'
publishDate: '2026-05-26'
tags:
diff --git a/packages/app/content/blog/zh/b200-nvfp4-vs-h200-int4-kimi-k2-vllm-perf-per-dollar.mdx b/packages/app/content/blog/zh/b200-nvfp4-vs-h200-int4-kimi-k2-vllm-perf-per-dollar.mdx
index 38e84357..04c09450 100644
--- a/packages/app/content/blog/zh/b200-nvfp4-vs-h200-int4-kimi-k2-vllm-perf-per-dollar.mdx
+++ b/packages/app/content/blog/zh/b200-nvfp4-vs-h200-int4-kimi-k2-vllm-perf-per-dollar.mdx
@@ -1,6 +1,7 @@
---
title: 'B200 NVFP4 对比 H200 INT4 运行 Kimi K2.5/K2.6:性价比提升高达 2.95 倍'
subtitle: '在 vLLM 8K/1K 工作负载下,B200 NVFP4 路径在 30–90 tok/s/user 推理区间内每百万 tokens 成本比 H200 INT4 低 2.71x–2.95x,比同一 B200 硬件上的 INT4 低 2.45x–2.74x。三个因素——B200 的 HBM 带宽、HBM 容量和 NVFP4 张量核心——可清晰分解该优势'
+seoDescription: '在 vLLM 8K/1K 下,B200 NVFP4 运行 Kimi K2.5/K2.6 每 token 成本比 H200 INT4 最多低 2.95 倍,覆盖 30–90 tok/s/user 全区间。'
date: '2026-05-26'
publishDate: '2026-05-26'
tags:
diff --git a/packages/app/content/blog/zh/gb200-nvl72-vs-b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx b/packages/app/content/blog/zh/gb200-nvl72-vs-b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx
index c1cd0c43..e70a6620 100644
--- a/packages/app/content/blog/zh/gb200-nvl72-vs-b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx
+++ b/packages/app/content/blog/zh/gb200-nvl72-vs-b200-disagg-deepseek-r1-fp4-dynamo-trt.mdx
@@ -1,6 +1,7 @@
---
title: 'GB200 NVL72 对比 B200 运行 DeepSeek R1 670B:在 125 tok/s/user 下每 GPU 吞吐量最高达 4.4 倍'
subtitle: 'DeepSeek R1 FP4 1k/1k。NVL72 的 72-GPU NVLink 扩展域允许解码使用最高 EP=32 的宽专家并行,而 B200 的 8-GPU NVLink 岛通过 RoCEv2 上限为 EP=8'
+seoDescription: 'GB200 NVL72 在 125 tok/s/user 下运行 DeepSeek R1 670B,每 GPU 吞吐量比 B200 最高提升 4.4 倍——72-GPU NVLink 支持 EP=32 宽专家并行。'
date: '2026-05-23'
publishDate: '2026-05-23'
tags:
diff --git a/packages/app/content/blog/zh/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx b/packages/app/content/blog/zh/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx
index 38fb02d1..9970710b 100644
--- a/packages/app/content/blog/zh/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx
+++ b/packages/app/content/blog/zh/gb300-nvl72-vs-gb200-nvl72-dsv4-pro-vllm-fp4.mdx
@@ -1,6 +1,7 @@
---
title: 'GB300 NVL72 vs GB200 NVL72 推理性能与性价比对比 — DeepSeek-V4-Pro 1.6T:吞吐量最高提升 2.83 倍'
subtitle: 'DSv4-Pro FP4 8K/1K,Dynamo+vLLM,两套机架均采用分离式部署。GB300 多出 50% 的 HBM(每 GPU 288 GB vs 192 GB)解锁了 GB200 无法容纳的更宽预填充+解码配方——尽管单 GPU TCO 溢价 20%,曲线中段性价比仍提升 2.31 倍。'
+seoDescription: 'GB300 NVL72 多出的 50% HBM 使 DeepSeek-V4-Pro 性价比比 GB200 NVL72 最高提升 2.31 倍(vLLM FP4 8K/1K),每 GPU 吞吐量最高提升 2.83 倍。'
date: '2026-05-27'
publishDate: '2026-05-27'
tags:
diff --git a/packages/app/content/blog/zh/inferencemax-open-source-inference-benchmarking.mdx b/packages/app/content/blog/zh/inferencemax-open-source-inference-benchmarking.mdx
index ecad0f7f..4958133b 100644
--- a/packages/app/content/blog/zh/inferencemax-open-source-inference-benchmarking.mdx
+++ b/packages/app/content/blog/zh/inferencemax-open-source-inference-benchmarking.mdx
@@ -1,6 +1,7 @@
---
title: 'InferenceMAX:开源推理基准测试'
subtitle: 'NVIDIA GB200 NVL72、AMD MI355X、每 GPU 吞吐量 Token、延迟 Tok/s/user、性价比、每百万 Token 成本、每配置兆瓦 Token 数、DeepSeek R1 670B、GPTOSS 120B、Llama3 70B'
+seoDescription: 'InferenceMAX:面向 NVIDIA GB200 NVL72 与 AMD MI355X 的开源推理基准测试——吞吐量、延迟、每 token 成本与每瓦性能。'
date: '2025-10-09'
publishDate: '2025-10-09'
tags:
diff --git a/packages/app/content/blog/zh/mi355x-deepseek-v4-pro-sglang-110x-in-26-days.mdx b/packages/app/content/blog/zh/mi355x-deepseek-v4-pro-sglang-110x-in-26-days.mdx
index 8a6134f2..a95695ad 100644
--- a/packages/app/content/blog/zh/mi355x-deepseek-v4-pro-sglang-110x-in-26-days.mdx
+++ b/packages/app/content/blog/zh/mi355x-deepseek-v4-pro-sglang-110x-in-26-days.mdx
@@ -1,6 +1,7 @@
---
title: 'MI355X 上 DeepSeek-V4-Pro 搭配 SGLang:26 天内每 GPU 吞吐量提升 110.5 倍'
subtitle: 'amd/deepseek_v4 分支合入了 TileLang 注意力索引器、Triton 稀疏 MLA、融合 RoPE/Hadamard、FlyDSL MoE 以及 FP4 权重,历经 31 个性能优化 PR——将首次点亮时 20 tok/s/GPU、2.4 tok/s/user 的水平提升至 8K/1K 负载下 2,256 tok/s/GPU、9.4 tok/s/user,吞吐量与交互性同步攀升'
+seoDescription: 'MI355X 在 SGLang 上 26 天内将 DeepSeek-V4-Pro 的每 GPU 吞吐量提升 110.5 倍——8K/1K 下经 TileLang、稀疏 MLA 与 FP4 从 20 提升至 2,256 tok/s/GPU。'
date: '2026-05-26'
publishDate: '2026-05-26'
tags:
diff --git a/packages/app/content/blog/zh/mi355x-glm5-fp8-sglang-40-cheaper-than-b200.mdx b/packages/app/content/blog/zh/mi355x-glm5-fp8-sglang-40-cheaper-than-b200.mdx
index a4817108..54067049 100644
--- a/packages/app/content/blog/zh/mi355x-glm5-fp8-sglang-40-cheaper-than-b200.mdx
+++ b/packages/app/content/blog/zh/mi355x-glm5-fp8-sglang-40-cheaper-than-b200.mdx
@@ -1,6 +1,7 @@
---
title: 'AMD MI355X GLM-5 推理:SGLang FP8 单节点每百万 token 成本比 B200 最高低 40%'
subtitle: 'GLM-5 发布 14 周后,AMD 在 MI355X 上同时实现了 SGLang FP8 的 MTP 和非 MTP 方案 — 通过 TileLang 实现的融合 MLA + FP8 KV 缓存在大部分性能 Pareto 前沿上将单节点 FP8 成本曲线翻转为 AMD 占优'
+seoDescription: 'AMD MI355X 在 SGLang FP8 上运行 GLM-5,每百万 token 成本比 B200 最高低 40%——TileLang 融合 MLA 与 FP8 KV 缓存。'
date: '2026-05-25'
publishDate: '2026-05-25'
tags:
diff --git a/packages/app/content/blog/zh/mi355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx b/packages/app/content/blog/zh/mi355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx
index ac7908b2..5e2e6738 100644
--- a/packages/app/content/blog/zh/mi355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx
+++ b/packages/app/content/blog/zh/mi355x-qwen3-5-sglang-v0-5-12-up-to-17x.mdx
@@ -1,6 +1,7 @@
---
title: 'AMD MI355X Qwen3.5 397B-A17B 推理:SGLang FP8 三个月内每 GPU 吞吐量提升最高 19 倍'
subtitle: '从 v0.5.8(2 月)→ v0.5.10rc0(4 月)→ v0.5.12(5 月),三次 AITER 内核合入 MI355X 加上从 TP=8 到 TP=2/TP=4 的重新调优,将 Qwen3.5 8k/1k 峰值从 1.3k 推高至 6.4k tok/s/GPU,并将曲线延伸至 75 tok/s/user'
+seoDescription: 'AMD MI355X 在 SGLang FP8 上 3 个月内将 Qwen3.5 397B-A17B 每 GPU 吞吐量最高提升 19 倍——经 AITER 内核从 1.3k 提升至 6.4k tok/s/GPU。'
date: '2026-05-25'
publishDate: '2026-05-25'
tags:
diff --git a/packages/app/src/app/blog/[slug]/page.tsx b/packages/app/src/app/blog/[slug]/page.tsx
index 05bda7e6..e21b5b12 100644
--- a/packages/app/src/app/blog/[slug]/page.tsx
+++ b/packages/app/src/app/blog/[slug]/page.tsx
@@ -16,6 +16,7 @@ import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-butt
import { Card } from '@/components/ui/card';
import { JsonLd } from '@/components/json-ld';
import {
+ blogDescription,
getAllPosts,
getAdjacentPosts,
extractHeadings,
@@ -43,10 +44,14 @@ export async function generateMetadata({ params }: Props): Promise {
const result = getPostBySlug(slug);
if (!result) return {};
const { meta } = result;
+ const description = blogDescription(meta);
return {
- title: meta.title,
- description: meta.subtitle,
+ // `absolute` keeps a short " | InferenceX" suffix instead of the long
+ // "%s | InferenceX by SemiAnalysis" root template, leaving more room for
+ // the headline before Google truncates the SERP title.
+ title: { absolute: `${meta.title} | ${SITE_NAME}` },
+ description,
keywords: meta.tags,
authors: [{ name: AUTHOR_NAME }],
alternates: {
@@ -56,7 +61,7 @@ export async function generateMetadata({ params }: Props): Promise {
},
openGraph: {
title: `${meta.title} | ${SITE_NAME}`,
- description: meta.subtitle,
+ description,
url: `${SITE_URL}/blog/${slug}`,
type: 'article',
publishedTime: `${meta.date}T00:00:00Z`,
@@ -67,7 +72,7 @@ export async function generateMetadata({ params }: Props): Promise {
twitter: {
card: 'summary_large_image',
title: meta.title,
- description: meta.subtitle,
+ description,
site: AUTHOR_HANDLE,
creator: AUTHOR_HANDLE,
},
diff --git a/packages/app/src/app/compare/[slug]/page.tsx b/packages/app/src/app/compare/[slug]/page.tsx
index 763b000f..b11011d5 100644
--- a/packages/app/src/app/compare/[slug]/page.tsx
+++ b/packages/app/src/app/compare/[slug]/page.tsx
@@ -1,12 +1,7 @@
import type { Metadata } from 'next';
import { notFound, permanentRedirect } from 'next/navigation';
-import {
- HW_REGISTRY,
- SITE_NAME,
- SITE_URL,
- SUPPORTERS_LINE,
-} from '@semianalysisai/inferencex-constants';
+import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants';
import { JsonLd } from '@/components/json-ld';
import { languageAlternates } from '@/lib/i18n';
@@ -15,11 +10,14 @@ import {
canonicalCompareSlug,
compareDisplayLabel,
compareModelDisplayLabel,
+ compareModelSeoName,
+ compareSeoTitle,
parseCompareSlug,
} from '@/lib/compare-slug';
import {
buildBreadcrumbJsonLd,
buildJsonLd,
+ compareMetaDescription,
compareTableNarrative,
computeCompareTableData,
dateRangeForPair,
@@ -46,16 +44,30 @@ export async function generateMetadata({ params }: Props): Promise {
if (!parsed) return {};
const fullLabel = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b);
const gpuLabel = compareDisplayLabel(parsed.a, parsed.b);
- const url = `${SITE_URL}/compare/${canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b)}`;
- const description = `${gpuLabel} inference benchmark on ${parsed.model.label}: verified, reproducible head-to-head results from InferenceX, the independent open-source GPU benchmark by SemiAnalysis. ${SUPPORTERS_LINE} Compare latency, throughput & cost.`;
+ const modelSeoName = compareModelSeoName(parsed.model);
+ const canonical = canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b);
+ const url = `${SITE_URL}/compare/${canonical}`;
+
+ // Lead the SEO title with the GPU pair — that's the phrase people search
+ // ("b200 vs b300") and must survive Google's ~60-char SERP truncation. The
+ // `absolute` form bypasses the long "%s | InferenceX by SemiAnalysis" root
+ // template so the query isn't pushed off the end.
+ const title = `${compareSeoTitle(gpuLabel, modelSeoName)} | ${SITE_NAME}`;
+
+ // Stat-led meta description built from the interpolated head-to-head numbers
+ // at the slug's default operating point (falls back to boilerplate for
+ // sparse-data pairs). Fetch is blob-cached and shared with the page render.
+ const rows = await getCachedBenchmarks(parsed.model.dbKeys);
+ const { sequence, precision } = pickPairDefaults(rows, parsed.a, parsed.b);
+ const { ssrRows } = computeCompareTableData(rows, parsed.a, parsed.b, sequence, precision);
+ const description = compareMetaDescription(parsed.model, parsed.a, parsed.b, ssrRows);
+
return {
- title: `${fullLabel} Inference Benchmark`,
+ title: { absolute: title },
description,
alternates: {
canonical: url,
- languages: languageAlternates(
- `/compare/${canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b)}`,
- ),
+ languages: languageAlternates(`/compare/${canonical}`),
},
openGraph: {
title: `${fullLabel} | ${SITE_NAME}`,
diff --git a/packages/app/src/app/zh/blog/[slug]/page.tsx b/packages/app/src/app/zh/blog/[slug]/page.tsx
index 85b93660..391b90a3 100644
--- a/packages/app/src/app/zh/blog/[slug]/page.tsx
+++ b/packages/app/src/app/zh/blog/[slug]/page.tsx
@@ -16,7 +16,13 @@ import { ReadingProgressBar } from '@/components/blog/reading-progress-bar';
import { ShareTwitterButton, ShareLinkedInButton } from '@/components/share-buttons';
import { Card } from '@/components/ui/card';
import { JsonLd } from '@/components/json-ld';
-import { getAllPosts, getAdjacentPosts, extractHeadings, getPostBySlug } from '@/lib/blog';
+import {
+ blogDescription,
+ getAllPosts,
+ getAdjacentPosts,
+ extractHeadings,
+ getPostBySlug,
+} from '@/lib/blog';
import { ZH_LANG_TAG, ZH_OG_LOCALE, zhAlternates } from '@/lib/i18n';
import {
AUTHOR_HANDLE,
@@ -38,16 +44,19 @@ export async function generateMetadata({ params }: Props): Promise {
const result = getPostBySlug(slug, 'zh');
if (!result) return {};
const { meta } = result;
+ const description = blogDescription(meta);
return {
- title: meta.title,
- description: meta.subtitle,
+ // Short " | InferenceX" suffix via `absolute` (mirrors the English page)
+ // so the headline keeps more of the SERP title before truncation.
+ title: { absolute: `${meta.title} | ${SITE_NAME}` },
+ description,
keywords: meta.tags,
authors: [{ name: AUTHOR_NAME }],
alternates: zhAlternates(`/blog/${slug}`),
openGraph: {
title: `${meta.title} | ${SITE_NAME}`,
- description: meta.subtitle,
+ description,
url: `${SITE_URL}/zh/blog/${slug}`,
type: 'article',
locale: ZH_OG_LOCALE,
@@ -59,7 +68,7 @@ export async function generateMetadata({ params }: Props): Promise {
twitter: {
card: 'summary_large_image',
title: meta.title,
- description: meta.subtitle,
+ description,
site: AUTHOR_HANDLE,
creator: AUTHOR_HANDLE,
},
diff --git a/packages/app/src/app/zh/compare/[slug]/page.tsx b/packages/app/src/app/zh/compare/[slug]/page.tsx
index 4cd11d03..2dae1257 100644
--- a/packages/app/src/app/zh/compare/[slug]/page.tsx
+++ b/packages/app/src/app/zh/compare/[slug]/page.tsx
@@ -1,12 +1,7 @@
import type { Metadata } from 'next';
import { notFound, permanentRedirect } from 'next/navigation';
-import {
- HW_REGISTRY,
- SITE_NAME,
- SITE_URL,
- SUPPORTERS_LINE_ZH,
-} from '@semianalysisai/inferencex-constants';
+import { HW_REGISTRY, SITE_NAME, SITE_URL } from '@semianalysisai/inferencex-constants';
import { JsonLd } from '@/components/json-ld';
import { pickPairDefaults } from '@/lib/compare-pair-defaults';
@@ -14,6 +9,7 @@ import {
canonicalCompareSlug,
compareDisplayLabel,
compareModelDisplayLabel,
+ compareModelSeoName,
parseCompareSlug,
} from '@/lib/compare-slug';
import {
@@ -29,6 +25,7 @@ import {
import {
buildBreadcrumbJsonLdZh,
buildJsonLdZh,
+ compareMetaDescriptionZh,
compareTableNarrativeZh,
} from '@/lib/compare-ssr-zh';
import { ZH_OG_LOCALE, zhAlternates } from '@/lib/i18n';
@@ -48,11 +45,23 @@ export async function generateMetadata({ params }: Props): Promise {
if (!parsed) return {};
const fullLabel = compareModelDisplayLabel(parsed.model, parsed.a, parsed.b);
const gpuLabel = compareDisplayLabel(parsed.a, parsed.b);
+ const modelSeoName = compareModelSeoName(parsed.model);
const canonical = canonicalCompareSlug(parsed.model.slug, parsed.a, parsed.b);
const url = `${SITE_URL}/zh/compare/${canonical}`;
- const description = `${gpuLabel} 在 ${parsed.model.label} 上的推理基准测试:来自 InferenceX(SemiAnalysis 推出的独立开源 GPU 基准测试平台)的经验证、可复现的正面对比结果。${SUPPORTERS_LINE_ZH}对比延迟、吞吐量与成本。`;
+
+ // GPU-pair-first title (mirrors the English page) via `absolute` so the long
+ // root template doesn't bury the search phrase. Model name / SKUs stay English.
+ const title = `${gpuLabel}:${modelSeoName} 推理基准测试 | ${SITE_NAME}`;
+
+ // Stat-led Chinese meta description from the interpolated head-to-head numbers
+ // at the slug's default operating point (falls back to boilerplate when thin).
+ const rows = await getCachedBenchmarks(parsed.model.dbKeys);
+ const { sequence, precision } = pickPairDefaults(rows, parsed.a, parsed.b);
+ const { ssrRows } = computeCompareTableData(rows, parsed.a, parsed.b, sequence, precision);
+ const description = compareMetaDescriptionZh(parsed.model, parsed.a, parsed.b, ssrRows);
+
return {
- title: `${fullLabel} 推理基准测试`,
+ title: { absolute: title },
description,
alternates: zhAlternates(`/compare/${canonical}`),
openGraph: {
diff --git a/packages/app/src/lib/blog.test.ts b/packages/app/src/lib/blog.test.ts
index 67633e87..e74fb1ea 100644
--- a/packages/app/src/lib/blog.test.ts
+++ b/packages/app/src/lib/blog.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs';
import {
+ blogDescription,
extractHeadings,
getAdjacentPosts,
getAllPosts,
@@ -9,6 +10,7 @@ import {
getReadingTime,
hasZhTranslation,
slugify,
+ smartTruncate,
} from './blog';
const FAKE_MDX = `---
@@ -179,6 +181,60 @@ describe('getReadingTime', () => {
});
});
+describe('smartTruncate', () => {
+ it('returns the text unchanged (no ellipsis) when at or under the limit', () => {
+ expect(smartTruncate('Short and sweet.', 155)).toBe('Short and sweet.');
+ const exact = 'x'.repeat(20);
+ expect(smartTruncate(exact, 20)).toBe(exact);
+ expect(smartTruncate(' trimmed ', 155)).toBe('trimmed');
+ });
+
+ it('cuts at a word boundary (never mid-word) and appends an ellipsis', () => {
+ const text = Array.from({ length: 60 }, () => 'word').join(' '); // 60×"word " → 299 chars
+ const out = smartTruncate(text, 155);
+ expect(out.length).toBeLessThanOrEqual(155);
+ expect(out.endsWith('…')).toBe(true);
+ const body = out.slice(0, -1);
+ // Every retained token is a complete "word" — no "wor"/"rd" fragments.
+ expect(body.split(' ').every((t) => t === 'word')).toBe(true);
+ });
+
+ it('strips trailing punctuation before the ellipsis', () => {
+ const text = `${'alpha, '.repeat(40)}beta`; // lands the cut right after a comma
+ const out = smartTruncate(text, 50);
+ const body = out.slice(0, -1);
+ expect(out.endsWith('…')).toBe(true);
+ expect(/[\s,]$/u.test(body)).toBe(false);
+ expect(out.length).toBeLessThanOrEqual(50);
+ });
+
+ it('hard-cuts CJK prose (no spaces) and stays within the limit', () => {
+ const cjk = '推'.repeat(200);
+ const out = smartTruncate(cjk, 155);
+ expect(out.length).toBeLessThanOrEqual(155);
+ expect(out.endsWith('…')).toBe(true);
+ });
+});
+
+describe('blogDescription', () => {
+ it('returns the explicit seoDescription verbatim when present', () => {
+ const meta = { seoDescription: 'Hand-written, punchy, ≤155 chars.', subtitle: 'x'.repeat(300) };
+ expect(blogDescription(meta)).toBe('Hand-written, punchy, ≤155 chars.');
+ });
+
+ it('smart-truncates the subtitle to ≤155 chars when no seoDescription is set', () => {
+ const subtitle = Array.from({ length: 60 }, () => 'word').join(' ');
+ const out = blogDescription({ subtitle });
+ expect(out).toBe(smartTruncate(subtitle, 155));
+ expect(out.length).toBeLessThanOrEqual(155);
+ expect(out.endsWith('…')).toBe(true);
+ });
+
+ it('passes a short subtitle through without an ellipsis', () => {
+ expect(blogDescription({ subtitle: 'A concise subtitle.' })).toBe('A concise subtitle.');
+ });
+});
+
describe('getAllPosts', () => {
it('returns an array of posts sorted by date descending', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
diff --git a/packages/app/src/lib/blog.ts b/packages/app/src/lib/blog.ts
index b69885ff..8fee5184 100644
--- a/packages/app/src/lib/blog.ts
+++ b/packages/app/src/lib/blog.ts
@@ -9,6 +9,11 @@ export interface BlogFrontmatter {
subtitle: string;
modifiedDate?: string;
publishDate?: string;
+ /** Optional hand-written SEO/social meta description. When set it overrides
+ * the auto-truncated `subtitle` for the SERP snippet — use it on posts whose
+ * `subtitle` runs past ~155 chars and would otherwise truncate mid-sentence.
+ * Keep it ≤155 chars (English) / ≤~78 CJK chars and compelling. */
+ seoDescription?: string;
tags?: string[];
}
@@ -49,6 +54,37 @@ export function getReadingTime(content: string): number {
return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE + cjkChars / CJK_CHARS_PER_MINUTE));
}
+/** Trailing whitespace + punctuation (any script), so a truncated snippet
+ * doesn't end in a dangling comma / dash / open bracket before the ellipsis. */
+const TRAILING_PUNCT_REGEX = /[\s\p{P}]+$/u;
+
+/**
+ * Truncate to at most `max` characters (ellipsis included) without cutting a
+ * word in half. Cuts at the last space that fits, strips trailing punctuation,
+ * and appends "…" only when the text was actually shortened. CJK prose has no
+ * spaces, so it falls back to a hard character cut (which is correct — there
+ * are no word boundaries to preserve).
+ */
+export function smartTruncate(text: string, max: number): string {
+ const clean = text.trim();
+ if (clean.length <= max) return clean;
+ // Reserve one char for the ellipsis so the result is guaranteed ≤ max.
+ const slice = clean.slice(0, max - 1);
+ const lastSpace = slice.lastIndexOf(' ');
+ const boundary = lastSpace > 0 ? slice.slice(0, lastSpace) : slice;
+ return `${boundary.replace(TRAILING_PUNCT_REGEX, '')}…`;
+}
+
+/**
+ * SERP / social meta description for a post: the explicit `seoDescription`
+ * frontmatter when present, otherwise the `subtitle` smart-truncated to a
+ * SERP-safe length. Used for the ``, OpenGraph, and
+ * Twitter descriptions.
+ */
+export function blogDescription(meta: Pick): string {
+ return meta.seoDescription ?? smartTruncate(meta.subtitle, 155);
+}
+
export function getAllPosts(locale: BlogLocale = 'en'): BlogPostMeta[] {
if (!fs.existsSync(CONTENT_DIR)) return [];
diff --git a/packages/app/src/lib/compare-slug.test.ts b/packages/app/src/lib/compare-slug.test.ts
index f85aac9e..c0c28cf1 100644
--- a/packages/app/src/lib/compare-slug.test.ts
+++ b/packages/app/src/lib/compare-slug.test.ts
@@ -6,6 +6,8 @@ import {
canonicalCompareSlug,
compareDisplayLabel,
compareModelDisplayLabel,
+ compareModelSeoName,
+ compareSeoTitle,
COMPARE_MODEL_ALIASES,
COMPARE_MODEL_SLUGS,
getCompareModelBySlug,
@@ -258,6 +260,69 @@ describe('compareModelDisplayLabel', () => {
});
});
+describe('compareModelSeoName', () => {
+ // Single, natural, current-version search name per model — no version-group
+ // slashes, no param-count suffix, no fully-qualified checkpoint id.
+ const EXPECTED: Record = {
+ 'deepseek-v4': 'DeepSeek V4 Pro',
+ 'deepseek-r1': 'DeepSeek R1',
+ 'kimi-k26': 'Kimi K2.6',
+ 'glm-5-1': 'GLM-5',
+ 'glm-5-2': 'GLM-5.2',
+ 'minimax-m3': 'MiniMax M3',
+ 'minimax-m27': 'MiniMax M2.7',
+ 'qwen-3-5': 'Qwen3.5',
+ 'gptoss-120b': 'gpt-oss-120b',
+ 'llama-3-3-70b': 'Llama 3.3 70B',
+ };
+
+ it('returns the expected single-version name for every model', () => {
+ for (const model of COMPARE_MODEL_SLUGS) {
+ expect(compareModelSeoName(model), `seoName for ${model.slug}`).toBe(EXPECTED[model.slug]);
+ }
+ });
+
+ it('covers every model in the registry (no missing entries)', () => {
+ expect(Object.keys(EXPECTED).toSorted()).toEqual(
+ COMPARE_MODEL_SLUGS.map((m) => m.slug).toSorted(),
+ );
+ });
+
+ it('never returns a version-grouped label (no slashes) and is non-empty', () => {
+ for (const model of COMPARE_MODEL_SLUGS) {
+ const name = compareModelSeoName(model);
+ expect(name.length, `non-empty for ${model.slug}`).toBeGreaterThan(0);
+ expect(name.includes('/'), `no slash in ${model.slug}`).toBe(false);
+ }
+ });
+});
+
+describe('compareSeoTitle', () => {
+ it('leads with the GPU pair and keeps "Inference Benchmark" when it fits', () => {
+ const title = compareSeoTitle('B200 vs B300', 'GLM-5');
+ expect(title).toBe('B200 vs B300: GLM-5 Inference Benchmark');
+ expect(title.startsWith('B200 vs B300:')).toBe(true);
+ expect(title.length).toBeLessThanOrEqual(60);
+ });
+
+ it('drops "Inference" → "Benchmark" for a long GPU pair + model name', () => {
+ const title = compareSeoTitle('GB200 NVL72 vs GB300 NVL72', 'DeepSeek V4 Pro');
+ // "…Inference Benchmark" would exceed ~60 chars, so it shortens.
+ expect(title).toBe('GB200 NVL72 vs GB300 NVL72: DeepSeek V4 Pro Benchmark');
+ expect(title.includes('Inference Benchmark')).toBe(false);
+ expect(title.startsWith('GB200 NVL72 vs GB300 NVL72:')).toBe(true);
+ });
+
+ it('always starts with the GPU pair for every real (model × pair)', () => {
+ for (const model of COMPARE_MODEL_SLUGS) {
+ const gpuLabel = compareDisplayLabel('gb200', 'gb300');
+ const title = compareSeoTitle(gpuLabel, compareModelSeoName(model));
+ expect(title.startsWith(`${gpuLabel}:`), `pair-led for ${model.slug}`).toBe(true);
+ expect(/(?Inference Benchmark|Benchmark)$/u.test(title)).toBe(true);
+ }
+ });
+});
+
describe('getCompareModelBySlug', () => {
it('returns canonical models for canonical slugs', () => {
expect(getCompareModelBySlug('deepseek-r1')).toBe(DEEPSEEK_R1);
diff --git a/packages/app/src/lib/compare-slug.ts b/packages/app/src/lib/compare-slug.ts
index 10e9ed4f..fb636c40 100644
--- a/packages/app/src/lib/compare-slug.ts
+++ b/packages/app/src/lib/compare-slug.ts
@@ -28,6 +28,13 @@ export interface CompareModelSlug {
dbKeys: string[];
/** Human label for OG image, metadata, and page header. */
label: string;
+ /** Single, natural, current-version model name for SEO `` tags and
+ * meta descriptions — e.g. `GLM-5` (not the version-grouped `GLM 5/5.1`)
+ * or `Kimi K2.6` (the primary version the slug represents). Kept short so
+ * the GPU-pair-led title survives Google's ~60-char SERP truncation, and
+ * written the way people actually search rather than the fully-qualified
+ * HF checkpoint id. See `compareModelSeoName`. */
+ seoName: string;
}
// Order matches the master /compare and /compare-per-dollar index display:
@@ -43,12 +50,14 @@ export const COMPARE_MODEL_SLUGS: CompareModelSlug[] = [
displayName: 'DeepSeek-V4-Pro',
dbKeys: ['dsv4'],
label: 'DeepSeek V4 Pro 1.6T',
+ seoName: 'DeepSeek V4 Pro',
},
{
slug: 'deepseek-r1',
displayName: 'DeepSeek-R1-0528',
dbKeys: ['dsr1'],
label: 'DeepSeek R1',
+ seoName: 'DeepSeek R1',
},
{
slug: 'kimi-k26',
@@ -62,6 +71,8 @@ export const COMPARE_MODEL_SLUGS: CompareModelSlug[] = [
// surfaces every version so the URL doesn't read as "only K2.6". Param
// count appended so the label conveys model scale alongside the version.
label: 'Kimi K2.5/K2.6/K2.7-Code 1T',
+ // SEO name uses the primary version the slug canonicalizes to (K2.6).
+ seoName: 'Kimi K2.6',
},
{
slug: 'glm-5-1',
@@ -69,12 +80,14 @@ export const COMPARE_MODEL_SLUGS: CompareModelSlug[] = [
// GLM-5 and GLM-5.1 remain grouped under the stable canonical slug.
dbKeys: ['glm5.1', 'glm5'],
label: 'GLM 5/5.1',
+ seoName: 'GLM-5',
},
{
slug: 'glm-5-2',
displayName: 'GLM-5.2',
dbKeys: ['glm5.2'],
label: 'GLM 5.2',
+ seoName: 'GLM-5.2',
},
{
slug: 'minimax-m3',
@@ -84,6 +97,7 @@ export const COMPARE_MODEL_SLUGS: CompareModelSlug[] = [
// joining the minimax-m27 group.
dbKeys: ['minimaxm3'],
label: 'MiniMax M3 428B',
+ seoName: 'MiniMax M3',
},
{
slug: 'minimax-m27',
@@ -91,6 +105,8 @@ export const COMPARE_MODEL_SLUGS: CompareModelSlug[] = [
// Same point-release grouping pattern as Kimi and GLM.
dbKeys: ['minimaxm2.7', 'minimaxm2.5'],
label: 'MiniMax M2.5/M2.7',
+ // Primary version the slug canonicalizes to (M2.7).
+ seoName: 'MiniMax M2.7',
},
{
slug: 'qwen-3-5',
@@ -98,18 +114,21 @@ export const COMPARE_MODEL_SLUGS: CompareModelSlug[] = [
dbKeys: ['qwen3.5'],
// 397B total parameters, 17B active per forward pass (MoE).
label: 'Qwen 3.5 397B-A17B',
+ seoName: 'Qwen3.5',
},
{
slug: 'gptoss-120b',
displayName: 'gpt-oss-120b',
dbKeys: ['gptoss120b'],
label: 'gpt-oss 120B',
+ seoName: 'gpt-oss-120b',
},
{
slug: 'llama-3-3-70b',
displayName: 'Llama-3.3-70B-Instruct-FP8',
dbKeys: ['llama70b'],
label: 'Llama 3.3 70B',
+ seoName: 'Llama 3.3 70B',
},
];
@@ -272,3 +291,25 @@ export function compareDisplayLabel(a: string, b: string): string {
export function compareModelDisplayLabel(model: CompareModelSlug, a: string, b: string): string {
return `${model.label} — ${compareDisplayLabel(a, b)}`;
}
+
+/** Single natural, current-version model name for SEO titles / descriptions —
+ * e.g. `GLM-5`, `Kimi K2.6`, `DeepSeek R1`. Unlike `label`, it never contains
+ * version-group slashes ("GLM 5/5.1"), param-count suffixes ("1.6T"), or the
+ * fully-qualified checkpoint id — it reads the way people search. */
+export function compareModelSeoName(model: CompareModelSlug): string {
+ return model.seoName;
+}
+
+/** Pre-pipe portion of the SEO ``, GPU-pair first so the actual search
+ * query ("B200 vs B300") survives Google's ~60-char truncation. Falls back
+ * from "Inference Benchmark" to "Benchmark" when the longer suffix would push
+ * the pair-led phrase past ~60 chars for a long GPU pair / model name.
+ *
+ * Returns the text before the " | InferenceX" site-name suffix; callers add
+ * the suffix via `title: { absolute: ... }` so the long root template
+ * ("%s | InferenceX by SemiAnalysis") does not apply and truncate the query. */
+export function compareSeoTitle(gpuLabel: string, modelSeoName: string): string {
+ const full = `${gpuLabel}: ${modelSeoName} Inference Benchmark`;
+ if (full.length <= 60) return full;
+ return `${gpuLabel}: ${modelSeoName} Benchmark`;
+}
diff --git a/packages/app/src/lib/compare-ssr-zh.ts b/packages/app/src/lib/compare-ssr-zh.ts
index cd48611a..149f9fac 100644
--- a/packages/app/src/lib/compare-ssr-zh.ts
+++ b/packages/app/src/lib/compare-ssr-zh.ts
@@ -9,17 +9,25 @@ import {
AUTHOR_NAME,
AUTHOR_URL,
HW_REGISTRY,
+ SITE_NAME,
SITE_URL,
} from '@semianalysisai/inferencex-constants';
-import { type CompareModelSlug, compareModelDisplayLabel } from '@/lib/compare-slug';
+import {
+ type CompareModelSlug,
+ compareDisplayLabel,
+ compareModelDisplayLabel,
+ compareModelSeoName,
+} from '@/lib/compare-slug';
import {
bandFor,
type CompareJsonLdVariant,
+ computeCompareStat,
fmtCost,
fmtPctDelta,
type FullBoth,
jsonLdEntryFor,
+ META_DESCRIPTION_MAX,
type PairSummary,
type PerDollarBoth,
pickRotated,
@@ -274,6 +282,67 @@ export function compareTableNarrativeZh(
return paragraphs;
}
+// ---------------------------------------------------------------------------
+// SEO meta description — Chinese port of compareMetaDescription
+// ---------------------------------------------------------------------------
+
+/** First candidate ≤ max, or undefined. Local mirror of the private helper in
+ * compare-ssr.ts so the ladder logic stays identical between the two files. */
+function firstUnderZh(candidates: string[], max: number): string | undefined {
+ return candidates.find((c) => c.length <= max);
+}
+
+/** Simplified Chinese, stat-led, ≤155-char meta description for a
+ * `/zh/compare/` page. 1:1 port of `compareMetaDescription`: same
+ * representative-row stat (`computeCompareStat`), same fallback-to-boilerplate
+ * and brand-clause-drop ladders. Model name, GPU SKUs and units stay English
+ * per the translation rules; the connective prose is Chinese. */
+export function compareMetaDescriptionZh(
+ model: CompareModelSlug,
+ a: string,
+ b: string,
+ ssrRows: SsrInterpolatedRow[],
+): string {
+ const modelName = compareModelSeoName(model);
+ const gpuLabel = compareDisplayLabel(a, b);
+
+ const fallback =
+ firstUnderZh(
+ [
+ `${gpuLabel} 在 ${modelName} 上的推理基准测试:来自 ${SITE_NAME}(${AUTHOR_NAME} 出品)的经验证、可复现开源结果。对比延迟、吞吐量与成本。`,
+ `${gpuLabel} 在 ${modelName} 上的推理基准测试:来自 ${SITE_NAME}(${AUTHOR_NAME} 出品)的开源结果。`,
+ `${gpuLabel} 在 ${modelName} 上的推理基准测试,来自 ${SITE_NAME}。`,
+ `${gpuLabel} 在 ${modelName} 上的推理基准测试。`,
+ ],
+ META_DESCRIPTION_MAX,
+ ) ?? `${gpuLabel} 推理基准测试`.slice(0, META_DESCRIPTION_MAX);
+
+ const stat = computeCompareStat(a, b, ssrRows);
+ if (!stat) return fallback;
+
+ const tputClause =
+ stat.tputPct > 0 ? `${stat.faster} 每 GPU 吞吐量比 ${stat.slower} 高 ${stat.tputPct}%` : null;
+ const costClause = stat.costPct > 0 ? `${stat.cheaper} 每 token 成本低 ${stat.costPct}%` : null;
+
+ let core: string;
+ if (tputClause && costClause) core = `在 ${modelName} 上,${tputClause};${costClause}。`;
+ else if (tputClause) core = `在 ${modelName} 上,${tputClause}。`;
+ else if (costClause)
+ core = `在 ${modelName} 上,${stat.cheaper} 每 token 成本比 ${stat.pricier} 低 ${stat.costPct}%。`;
+ else return fallback;
+
+ return (
+ firstUnderZh(
+ [
+ `${core}来自 ${SITE_NAME}(${AUTHOR_NAME} 出品)的可验证开源基准测试。`,
+ `${core}来自 ${SITE_NAME} 的开源基准测试。`,
+ core,
+ ],
+ META_DESCRIPTION_MAX,
+ ) ?? fallback
+ );
+}
+
// ---------------------------------------------------------------------------
// JSON-LD — Chinese
// ---------------------------------------------------------------------------
diff --git a/packages/app/src/lib/compare-ssr.test.ts b/packages/app/src/lib/compare-ssr.test.ts
index 356fc869..be02369c 100644
--- a/packages/app/src/lib/compare-ssr.test.ts
+++ b/packages/app/src/lib/compare-ssr.test.ts
@@ -1,8 +1,17 @@
import { describe, expect, it } from 'vitest';
import type { BenchmarkRow } from '@/lib/api';
+import type { InterpolatedResult } from '@/components/calculator/types';
-import { computeCompareImageRows, KNOWN_MODELS } from './compare-ssr';
+import { COMPARE_MODEL_SLUGS } from './compare-slug';
+import {
+ compareMetaDescription,
+ computeCompareImageRows,
+ KNOWN_MODELS,
+ META_DESCRIPTION_MAX,
+ type SsrInterpolatedRow,
+} from './compare-ssr';
+import { compareMetaDescriptionZh } from './compare-ssr-zh';
// BenchmarkRow.id is required (stable per-point id from benchmark_results);
// hand out a fresh one per stub so id-keyed logic can't collide across rows.
@@ -141,3 +150,162 @@ describe('computeCompareImageRows', () => {
).toEqual([]);
});
});
+
+// ---------------------------------------------------------------------------
+// compareMetaDescription (+ zh port)
+// ---------------------------------------------------------------------------
+
+const GLM = COMPARE_MODEL_SLUGS.find((m) => m.slug === 'glm-5-1')!; // seoName 'GLM-5'
+const DSV4 = COMPARE_MODEL_SLUGS.find((m) => m.slug === 'deepseek-v4')!; // long seoName
+
+/** Minimal InterpolatedResult stub — compareMetaDescription only reads
+ * `value` (tok/s/GPU) and `cost` ($/M tok); the rest are inert zeros. */
+function ir(value: number, cost: number): InterpolatedResult {
+ return {
+ hwKey: 'x',
+ resultKey: 'x',
+ value,
+ outputTputValue: value,
+ inputTputValue: 0,
+ cost,
+ costInput: 0,
+ costOutput: cost,
+ tpPerMw: 0,
+ inputTpPerMw: 0,
+ outputTpPerMw: 0,
+ concurrency: 0,
+ nearestPoints: [],
+ };
+}
+
+function makeSsrRows(
+ triples: [number, InterpolatedResult | null, InterpolatedResult | null][],
+): SsrInterpolatedRow[] {
+ return triples.map(([target, a, b]) => ({ target, a, b }));
+}
+
+describe('compareMetaDescription', () => {
+ it('leads with the throughput + cost stat when both dimensions differ', () => {
+ // a=B200 34% faster, b=B300 12% cheaper, at the middle (default) target.
+ const ssr = makeSsrRows([
+ [20, ir(60, 2), ir(50, 1.7)],
+ [40, ir(134, 1.12), ir(100, 1)], // 134/100 = +34%, 1.12/1.0 = +12%
+ [60, ir(200, 0.9), ir(160, 0.85)],
+ ]);
+ const desc = compareMetaDescription(GLM, 'b200', 'b300', ssr);
+ expect(desc).toBe(
+ 'B200 delivers 34% more tok/s/GPU than B300 on GLM-5; B300 is 12% cheaper per token. Verified open-source benchmarks from InferenceX by SemiAnalysis.',
+ );
+ expect(desc.length).toBeLessThanOrEqual(META_DESCRIPTION_MAX);
+ expect(desc.startsWith('B200 delivers 34% more tok/s/GPU than B300 on GLM-5')).toBe(true);
+ });
+
+ it('emits only the throughput clause when cost is within 1% (tied)', () => {
+ const ssr = makeSsrRows([[40, ir(134, 1), ir(100, 1)]]);
+ const desc = compareMetaDescription(GLM, 'b200', 'b300', ssr);
+ expect(desc.includes('34% more tok/s/GPU')).toBe(true);
+ expect(desc.includes('cheaper per token')).toBe(false);
+ expect(desc.length).toBeLessThanOrEqual(META_DESCRIPTION_MAX);
+ });
+
+ it('emits only the cost clause (with "than") when throughput is tied', () => {
+ const ssr = makeSsrRows([[40, ir(100, 1.5), ir(100, 1)]]);
+ const desc = compareMetaDescription(GLM, 'b200', 'b300', ssr);
+ expect(desc).toContain('B300 is 50% cheaper per token than B200 on GLM-5');
+ expect(desc.includes('more tok/s/GPU')).toBe(false);
+ expect(desc.length).toBeLessThanOrEqual(META_DESCRIPTION_MAX);
+ });
+
+ it('falls back to boilerplate when there is no comparable data', () => {
+ const empty = compareMetaDescription(GLM, 'b200', 'b300', []);
+ expect(empty.startsWith('B200 vs B300 inference benchmark on GLM-5')).toBe(true);
+ expect(empty.includes('tok/s/GPU')).toBe(false);
+ expect(empty.length).toBeLessThanOrEqual(META_DESCRIPTION_MAX);
+
+ // One side missing at every target → still boilerplate.
+ const oneSided = compareMetaDescription(
+ GLM,
+ 'b200',
+ 'b300',
+ makeSsrRows([[40, ir(100, 1), null]]),
+ );
+ expect(oneSided.startsWith('B200 vs B300 inference benchmark on GLM-5')).toBe(true);
+ });
+
+ it('falls back when both dimensions are within 1%', () => {
+ const ssr = makeSsrRows([[40, ir(100, 1), ir(100, 1)]]);
+ const desc = compareMetaDescription(GLM, 'b200', 'b300', ssr);
+ expect(desc.startsWith('B200 vs B300 inference benchmark on GLM-5')).toBe(true);
+ });
+
+ it('prefers the middle (default) target row', () => {
+ // Middle row has the data; outer rows are degenerate.
+ const ssr = makeSsrRows([
+ [20, null, null],
+ [40, ir(134, 1.12), ir(100, 1)],
+ [60, null, null],
+ ]);
+ const desc = compareMetaDescription(GLM, 'b200', 'b300', ssr);
+ expect(desc.includes('34% more tok/s/GPU')).toBe(true);
+ });
+
+ it('falls back to an outer usable row when the middle target is degenerate', () => {
+ const ssr = makeSsrRows([
+ [20, ir(134, 1.12), ir(100, 1)],
+ [40, null, null],
+ [60, null, null],
+ ]);
+ const desc = compareMetaDescription(GLM, 'b200', 'b300', ssr);
+ expect(desc.includes('34% more tok/s/GPU')).toBe(true);
+ });
+
+ it('stays ≤155 chars even with long GPU labels, model name, and huge ratios', () => {
+ const ssr = makeSsrRows([[40, ir(9999, 99.9), ir(10, 0.5)]]);
+ const desc = compareMetaDescription(DSV4, 'gb200', 'gb300', ssr);
+ expect(desc.length).toBeLessThanOrEqual(META_DESCRIPTION_MAX);
+ // A differentiating stat is still present (not the boilerplate).
+ expect(desc.includes('tok/s/GPU') || desc.includes('cheaper per token')).toBe(true);
+ });
+});
+
+describe('compareMetaDescriptionZh — structural 1:1 port', () => {
+ const ssr = makeSsrRows([
+ [20, ir(60, 2), ir(50, 1.7)],
+ [40, ir(134, 1.12), ir(100, 1)],
+ [60, ir(200, 0.9), ir(160, 0.85)],
+ ]);
+
+ it('surfaces the same numbers, model name, and GPU labels as the English version', () => {
+ const zh = compareMetaDescriptionZh(GLM, 'b200', 'b300', ssr);
+ expect(zh).toContain('GLM-5');
+ expect(zh).toContain('B200');
+ expect(zh).toContain('B300');
+ expect(zh).toContain('34%');
+ expect(zh).toContain('12%');
+ expect(zh).toContain('每 GPU 吞吐量');
+ expect(zh).toContain('每 token 成本');
+ expect(zh.length).toBeLessThanOrEqual(META_DESCRIPTION_MAX);
+ });
+
+ it('is stat-led exactly when the English version is (and boilerplate otherwise)', () => {
+ // With data: both stat-led.
+ const en = compareMetaDescription(GLM, 'b200', 'b300', ssr);
+ const zh = compareMetaDescriptionZh(GLM, 'b200', 'b300', ssr);
+ expect(en.includes('34%')).toBe(true);
+ expect(zh.includes('34%')).toBe(true);
+
+ // Without data: both boilerplate.
+ const enFb = compareMetaDescription(GLM, 'b200', 'b300', []);
+ const zhFb = compareMetaDescriptionZh(GLM, 'b200', 'b300', []);
+ expect(enFb.includes('inference benchmark')).toBe(true);
+ expect(zhFb.includes('推理基准测试')).toBe(true);
+ expect(zhFb.length).toBeLessThanOrEqual(META_DESCRIPTION_MAX);
+ });
+
+ it('stays ≤155 chars with long labels + huge ratios', () => {
+ const big = makeSsrRows([[40, ir(9999, 99.9), ir(10, 0.5)]]);
+ expect(compareMetaDescriptionZh(DSV4, 'gb200', 'gb300', big).length).toBeLessThanOrEqual(
+ META_DESCRIPTION_MAX,
+ );
+ });
+});
diff --git a/packages/app/src/lib/compare-ssr.ts b/packages/app/src/lib/compare-ssr.ts
index 44e80939..592a8238 100644
--- a/packages/app/src/lib/compare-ssr.ts
+++ b/packages/app/src/lib/compare-ssr.ts
@@ -14,6 +14,7 @@ import {
AUTHOR_NAME,
AUTHOR_URL,
HW_REGISTRY,
+ SITE_NAME,
SITE_URL,
sequenceToIslOsl,
} from '@semianalysisai/inferencex-constants';
@@ -35,6 +36,7 @@ import {
type ComparePair,
type CompareModelSlug,
compareModelDisplayLabel,
+ compareModelSeoName,
} from '@/lib/compare-slug';
import { getHardwareConfig, getGpuSpecs } from '@/lib/constants';
import { loadFixture } from '@/lib/test-fixtures';
@@ -726,6 +728,150 @@ export function compareTableNarrative(
return paragraphs;
}
+// ---------------------------------------------------------------------------
+// SEO meta description — stat-led, ≤155 chars, per (model, GPU pair)
+// ---------------------------------------------------------------------------
+
+/** The single interpolated head-to-head stat surfaced in the meta description.
+ * `tputPct` / `costPct` are 0 when that dimension is within ~1% (a tie). */
+export interface CompareStat {
+ aLabel: string;
+ bLabel: string;
+ gpuLabel: string;
+ faster: string;
+ slower: string;
+ /** Throughput-per-GPU advantage of `faster` over `slower`, as a whole
+ * percent (0 when the two are within 1%). */
+ tputPct: number;
+ cheaper: string;
+ pricier: string;
+ /** Cost-per-token advantage of `cheaper` over `pricier`, as a whole percent
+ * (0 when the two are within 1%). */
+ costPct: number;
+}
+
+/** Does a row carry a real, comparable data point for both GPUs? */
+function statRowUsable(row: SsrInterpolatedRow): boolean {
+ return Boolean(
+ row.a && row.b && row.a.value > 0 && row.b.value > 0 && row.a.cost > 0 && row.b.cost > 0,
+ );
+}
+
+/** Pick the representative interpolated row for the meta description — the
+ * default (middle) interactivity target if it has data for both GPUs, else
+ * the first target that does. Returns null when no target has a comparable
+ * pair (sparse-data pairs fall back to boilerplate). */
+function pickCompareStatRow(rows: SsrInterpolatedRow[]): SsrInterpolatedRow | null {
+ const mid = rows[Math.floor(rows.length / 2)];
+ if (mid && statRowUsable(mid)) return mid;
+ return rows.find(statRowUsable) ?? null;
+}
+
+/** Language-agnostic head-to-head stat for the meta description, computed at
+ * the default interactivity target. Shared by the English `compareMetaDescription`
+ * and its Chinese port so both surface the same numbers. Returns null when the
+ * pair has no comparable interpolated data. */
+export function computeCompareStat(
+ a: string,
+ b: string,
+ ssrRows: SsrInterpolatedRow[],
+): CompareStat | null {
+ const row = pickCompareStatRow(ssrRows);
+ if (!row || !row.a || !row.b) return null;
+
+ const aLabel = HW_REGISTRY[a]?.label ?? a.toUpperCase();
+ const bLabel = HW_REGISTRY[b]?.label ?? b.toUpperCase();
+ const rA = row.a;
+ const rB = row.b;
+
+ const aFaster = rA.value > rB.value;
+ const tputRatio = aFaster ? rA.value / rB.value : rB.value / rA.value;
+ const tputPct = Math.round((tputRatio - 1) * 100);
+
+ const aCheaper = rA.cost < rB.cost;
+ const costRatio = aCheaper ? rB.cost / rA.cost : rA.cost / rB.cost;
+ const costPct = Math.round((costRatio - 1) * 100);
+
+ return {
+ aLabel,
+ bLabel,
+ gpuLabel: compareDisplayLabel(a, b),
+ faster: aFaster ? aLabel : bLabel,
+ slower: aFaster ? bLabel : aLabel,
+ tputPct: tputPct >= 1 ? tputPct : 0,
+ cheaper: aCheaper ? aLabel : bLabel,
+ pricier: aCheaper ? bLabel : aLabel,
+ costPct: costPct >= 1 ? costPct : 0,
+ };
+}
+
+/** Length cap for meta descriptions — Google truncates SERP snippets past
+ * ~155–160 chars, so every branch below must stay at or under this. */
+export const META_DESCRIPTION_MAX = 155;
+
+/** First candidate that fits within `max`, or undefined if none do. */
+function firstUnder(candidates: string[], max: number): string | undefined {
+ return candidates.find((c) => c.length <= max);
+}
+
+/** Stat-led, ≤155-char meta description for a `/compare/` page. Leads
+ * with the differentiating throughput / cost delta at the default interactivity
+ * target (the actual reason a searcher clicks) instead of the boilerplate that
+ * was identical across ~360 pages. Falls back to a compact, still-≤155-char
+ * boilerplate sentence when the pair has thin / degenerate data.
+ *
+ * A trailing brand clause is dropped down a ladder (full → short → none) so a
+ * long GPU-pair / model name never pushes the description over the cap. */
+export function compareMetaDescription(
+ model: CompareModelSlug,
+ a: string,
+ b: string,
+ ssrRows: SsrInterpolatedRow[],
+): string {
+ const modelName = compareModelSeoName(model);
+ const gpuLabel = compareDisplayLabel(a, b);
+
+ // Boilerplate fallback ladder — progressively shorter so one always fits.
+ const fallback =
+ firstUnder(
+ [
+ `${gpuLabel} inference benchmark on ${modelName}: verified, reproducible open-source results from ${SITE_NAME} by ${AUTHOR_NAME}. Latency, throughput & cost.`,
+ `${gpuLabel} inference benchmark on ${modelName}: verified open-source results from ${SITE_NAME} by ${AUTHOR_NAME}.`,
+ `${gpuLabel} inference benchmark on ${modelName} from ${SITE_NAME} by ${AUTHOR_NAME}.`,
+ `${gpuLabel} inference benchmark on ${modelName}.`,
+ ],
+ META_DESCRIPTION_MAX,
+ ) ?? `${gpuLabel} inference benchmark`.slice(0, META_DESCRIPTION_MAX);
+
+ const stat = computeCompareStat(a, b, ssrRows);
+ if (!stat) return fallback;
+
+ const tputClause =
+ stat.tputPct > 0
+ ? `${stat.faster} delivers ${stat.tputPct}% more tok/s/GPU than ${stat.slower} on ${modelName}`
+ : null;
+ const costClause =
+ stat.costPct > 0 ? `${stat.cheaper} is ${stat.costPct}% cheaper per token` : null;
+
+ let core: string;
+ if (tputClause && costClause) core = `${tputClause}; ${costClause}.`;
+ else if (tputClause) core = `${tputClause}.`;
+ else if (costClause)
+ core = `${stat.cheaper} is ${stat.costPct}% cheaper per token than ${stat.pricier} on ${modelName}.`;
+ else return fallback; // both dimensions within 1% — nothing differentiating to lead with
+
+ return (
+ firstUnder(
+ [
+ `${core} Verified open-source benchmarks from ${SITE_NAME} by ${AUTHOR_NAME}.`,
+ `${core} Verified open-source ${SITE_NAME} benchmarks.`,
+ core,
+ ],
+ META_DESCRIPTION_MAX,
+ ) ?? fallback
+ );
+}
+
// ---------------------------------------------------------------------------
// Master-index helpers (shared by /compare and /compare-per-dollar)
// ---------------------------------------------------------------------------
diff --git a/packages/app/src/lib/compare-variant-ssr.test.ts b/packages/app/src/lib/compare-variant-ssr.test.ts
index 0e85c225..857e8ef0 100644
--- a/packages/app/src/lib/compare-variant-ssr.test.ts
+++ b/packages/app/src/lib/compare-variant-ssr.test.ts
@@ -159,6 +159,7 @@ const MODEL_SLUG = {
displayName: 'DeepSeek-R1-0528',
dbKeys: ['dsr1'],
label: 'DeepSeek R1',
+ seoName: 'DeepSeek R1',
};
// ---------------------------------------------------------------------------
From d3116d73233857e3f2d8187b050af4f87815999f Mon Sep 17 00:00:00 2001
From: Oseltamivir <58582368+Oseltamivir@users.noreply.github.com>
Date: Mon, 20 Jul 2026 17:04:39 +0800
Subject: [PATCH 2/2] fix(seo): sync blog JSON-LD description with meta/OG
description
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Cursor Bugbot flagged that the BlogPosting JSON-LD description still used
the full meta.subtitle while the HTML meta, OpenGraph, and Twitter
descriptions were routed through blogDescription — so posts with a
seoDescription or long subtitle exposed different text in structured data
than in the SERP/social tags. Route the JSON-LD description through the
same blogDescription helper (EN + zh) so they stay in sync. Logic is
already covered by the blogDescription unit tests.
中文:修复博客 JSON-LD 描述与 meta/OG 描述不一致的问题。Cursor Bugbot 指出
BlogPosting JSON-LD 的 description 仍使用完整的 meta.subtitle,而 HTML meta、
OpenGraph 和 Twitter 描述已改用 blogDescription,导致设置了 seoDescription 或
副标题较长的文章在结构化数据与搜索/社交标签中显示不同文本。这里让 JSON-LD 的
description 同样走 blogDescription(EN 与 zh 两侧),保持一致;相关逻辑已由
blogDescription 单元测试覆盖。
---
packages/app/src/app/blog/[slug]/page.tsx | 5 ++++-
packages/app/src/app/zh/blog/[slug]/page.tsx | 5 ++++-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/packages/app/src/app/blog/[slug]/page.tsx b/packages/app/src/app/blog/[slug]/page.tsx
index e21b5b12..eaad761c 100644
--- a/packages/app/src/app/blog/[slug]/page.tsx
+++ b/packages/app/src/app/blog/[slug]/page.tsx
@@ -144,7 +144,10 @@ export default async function BlogPostPage({ params }: Props) {
publisher: { '@type': 'Organization', name: AUTHOR_NAME },
datePublished: `${meta.date}T00:00:00Z`,
...(meta.modifiedDate && { dateModified: `${meta.modifiedDate}T00:00:00Z` }),
- description: meta.subtitle,
+ // Keep structured-data description in sync with the SERP/OG/Twitter meta
+ // (both go through blogDescription) so they never diverge for posts with a
+ // seoDescription or a long subtitle.
+ description: blogDescription(meta),
url: `${SITE_URL}/blog/${slug}`,
wordCount: raw.trim().split(/\s+/u).length,
timeRequired: `PT${meta.readingTime}M`,
diff --git a/packages/app/src/app/zh/blog/[slug]/page.tsx b/packages/app/src/app/zh/blog/[slug]/page.tsx
index 391b90a3..4d251d8b 100644
--- a/packages/app/src/app/zh/blog/[slug]/page.tsx
+++ b/packages/app/src/app/zh/blog/[slug]/page.tsx
@@ -140,7 +140,10 @@ export default async function ZhBlogPostPage({ params }: Props) {
publisher: { '@type': 'Organization', name: AUTHOR_NAME },
datePublished: `${meta.date}T00:00:00Z`,
...(meta.modifiedDate && { dateModified: `${meta.modifiedDate}T00:00:00Z` }),
- description: meta.subtitle,
+ // Keep structured-data description in sync with the SERP/OG/Twitter meta
+ // (both go through blogDescription) so they never diverge for posts with a
+ // seoDescription or a long subtitle.
+ description: blogDescription(meta),
url: `${SITE_URL}/zh/blog/${slug}`,
inLanguage: ZH_LANG_TAG,
wordCount: raw.trim().split(/\s+/u).length,