第二十一章:SEO 优化与元数据管理
第二十一章:SEO 优化与元数据管理
1. SPA 的 SEO 挑战
单页应用(SPA)的 SEO 面临几个核心问题:
| 问题 | 原因 | 后果 |
|------|------|------|
| 初始 HTML 为空 | JS 动态渲染,爬虫不执行 JS | 搜索引擎看到空白页 |
| 元数据静态 | <title> 在 index.html 中写死 | 所有页面共享同一标题描述 |
| 缺少社交标签 | OG/Twitter 标签不随路由变化 | 分享链接无预览 |
解决方案对比:
| 方案 | 优点 | 缺点 | |------|------|------| | SSR(服务端渲染) | 爬虫友好,最完整的 SEO | 需要 Node/Rust 服务器 | | Prerender(预渲染) | 部署简单,静态化 | 动态内容需额外配置 | | 动态 Head 管理 | 轻量,SPA 原生 | 爬虫兼容性需验证 | | 混合式(SSG + CSR) | 兼顾 SEO 和交互 | 构建流程复杂 |
2. 动态 Head 管理
use_head id="2-1-使用-管理标题">2.1 使用 管理标题
Dioxus 的 use_head 允许组件动态修改 <head> 中的内容:
use dioxus::prelude::*;
#[component]
fn BlogPostPage(slug: String) -> Element {
let article = use_resource(move || async move {
fetch_article(&slug).await
});
// 动态设置页面标题
use_hook(|| {
let title = article()
.as_ref()
.map(|a| format!("{} - Blog SSR", a.title))
.unwrap_or_else(|| "加载中...".to_string());
use_head().title.set_title(&title);
});
// ...
}
2.2 完整的元数据 Hook
use dioxus::prelude::*;
fn use_page_meta(title: String, description: String, image: Option<String>) {
use_effect(move || {
let head = use_head();
// 设置标题
head.title.set_title(&format!("{title} - Blog SSR"));
// 设置描述
head.links.push(HeadLink {
rel: "description".into(),
href: "".into(),
sizes: None,
media: None,
});
head.meta.push(HeadMeta {
name: Some("description".into()),
property: None,
content: Some(description.clone()),
});
// Open Graph 标签
let og_tags = vec![
("og:title", title.clone()),
("og:description", description),
("og:type", "article".to_string()),
("og:url", web_sys::window()
.and_then(|w| w.location().href().ok())
.unwrap_or_default()),
];
for (property, content) in og_tags {
head.meta.push(HeadMeta {
name: None,
property: Some(property.into()),
content: Some(content),
});
}
// OG 图片
if let Some(img) = image {
head.meta.push(HeadMeta {
name: None,
property: Some("og:image".into()),
content: Some(img),
});
}
// Twitter Card
head.meta.push(HeadMeta {
name: Some("twitter:card".into()),
property: None,
content: Some("summary_large_image".into()),
});
head.meta.push(HeadMeta {
name: Some("twitter:title".into()),
property: None,
content: Some(title),
});
});
}
// 使用
#[component]
fn ArticlePage(slug: String) -> Element {
let article = use_resource(move || async move {
fetch_article(&slug).await
});
if let Some(ref a) = article() {
use_page_meta(
a.title.clone(),
a.abstract_field.clone(),
Some(a.cover_image.clone()),
);
}
// ...
}
3. 结构化数据(Schema.org)
搜索引擎支持结构化数据来增强搜索结果展示。JSON-LD 是最推荐的方式:
fn use_json_ld(json: serde_json::Value) {
use_effect(move || {
let head = use_head();
// JSON-LD 通过 script 标签注入
head.links.push(HeadLink {
rel: "".into(),
href: "".into(),
sizes: None,
media: None,
});
// 实际注入方式使用 inner_html
});
}
// 生成 Article 结构化数据
fn article_schema(article: &ArticleDto) -> serde_json::Value {
serde_json::json!({
"@context": "https://schema.org",
"@type": "Article",
"headline": article.title,
"description": article.abstract_field,
"datePublished": article.created_at,
"dateModified": article.updated_at,
"author": {
"@type": "Person",
"name": "Rabit Logic",
"url": "https://github.com/RabitLogic"
},
"publisher": {
"@type": "Organization",
"name": "Blog SSR",
"logo": {
"@type": "ImageObject",
"url": "https://blog.example.com/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": format!("https://blog.example.com/article/{}", article.slug)
},
"image": article.cover_image
})
}
4. 面包屑导航
面包屑不仅改善用户体验,也是重要的 SEO 信号:
#[component]
fn Breadcrumb(items: Vec<BreadcrumbItem>) -> Element {
use_effect(move || {
// 注入 BreadcrumbList 结构化数据
let schema = serde_json::json!({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": items.iter().enumerate().map(|(i, item)| {
serde_json::json!({
"@type": "ListItem",
"position": i + 1,
"name": item.label,
"item": item.url
})
}).collect::<Vec<_>>()
});
// 写入 head
});
rsx! {
nav { class: "text-sm mb-4", "aria-label": "breadcrumb",
ol { class: "flex items-center gap-1.5",
for (i, item) in items.iter().enumerate() {
li { class: "flex items-center gap-1.5",
if i > 0 {
span { class: "text-gray-400", "/" }
}
if item.active {
span { class: "font-medium",
style: "color: var(--text);",
"{item.label}"
}
} else {
a {
class: "transition-colors",
style: "color: var(--tertiary);",
href: "{item.url}",
"{item.label}"
}
}
}
}
}
}
}
}
// 使用
#[component]
fn ArticlePage(slug: String) -> Element {
rsx! {
Breadcrumb {
items: vec![
BreadcrumbItem { label: "首页".into(), url: "/".into(), active: false },
BreadcrumbItem { label: "文章".into(), url: "/blog".into(), active: false },
BreadcrumbItem { label: slug.clone(), url: format!("/article/{slug}"), active: true },
],
}
// 文章内容
}
}
5. 站点地图
5.1 sitemap.xml 生成
SPA 需要生成 sitemap.xml 供搜索引擎爬取:
// 在服务端生成 sitemap.xml
async fn generate_sitemap() -> String {
let articles = load_all_articles();
let urls: Vec<String> = articles.iter().map(|a| {
format!(
r#" <url>
<loc>https://blog.example.com/article/{}</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>"#,
a.slug
)
}).collect();
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://blog.example.com/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://blog.example.com/blog</loc>
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
{}
</urlset>"#,
urls.join("\n")
)
}
5.2 robots.txt
# public/robots.txt
User-agent: *
Allow: /
Sitemap: https://blog.example.com/sitemap.xml
6. 页面加载性能与 SEO
搜索引擎考虑 Core Web Vitals:
| 指标 | 要求 | Dioxus 优化策略 | |------|------|----------------| | LCP | ≤ 2.5s | 关键 CSS 内联,图片懒加载 | | FID | ≤ 100ms | 代码分割,长任务拆分 | | CLS | ≤ 0.1 | 图片/广告预留尺寸,字体预加载 |
// 图片懒加载 —— 减少 LCP 阻塞
#[component]
fn LazyImage(src: String, alt: String) -> Element {
let loaded = use_signal(|| false);
let in_view = use_signal(|| false);
// IntersectionObserver 检测进入视口
use_effect(move || {
// 使用 web-sys 创建 observer
// 进入视口时设置 in_view = true
});
rsx! {
div {
class: "bg-gray-100 rounded overflow-hidden",
style: "aspect-ratio: 16/9;", // 预留尺寸防 CLS
if in_view() {
img {
src: "{src}",
alt: "{alt}",
class: "w-full h-full object-cover transition-opacity duration-300",
style: "opacity: {if loaded() { \"1\" } else { \"0\" }};",
onload: move |_| loaded.set(true),
}
}
}
}
}
// 字体预加载
#[component]
fn App() -> Element {
use_effect(move || {
let head = use_head();
head.links.push(HeadLink {
rel: "preload".into(),
href: "/assets/fonts/inter.woff2".into(),
sizes: None,
media: None,
});
});
rsx! { Router::<Route> {} }
}
7. 社交分享预览
7.1 动态 OG 图片
可以动态生成带文章标题的 OG 图片:
// 使用 Vercel OG 或自建服务生成社交分享图
fn og_image_url(title: &str, slug: &str) -> String {
format!(
"https://og.example.com/og?title={}&slug={}",
url::encode(title),
slug
)
}
7.2 分享按钮
#[component]
fn ShareButtons(title: String, url: String) -> Element {
let encoded_url = url::encode(&url);
let encoded_title = url::encode(&title);
rsx! {
div { class: "flex items-center gap-2",
span { class: "text-xs", style: "color: var(--tertiary);", "分享:" }
// Twitter
a {
class: "share-btn",
href: "https://twitter.com/intent/tweet?text={encoded_title}&url={encoded_url}",
target: "_blank",
"𝕏"
}
// 微信(显示二维码弹窗)
button {
class: "share-btn",
onclick: move |_| show_qrcode(url.clone()),
"微信"
}
// 复制链接
button {
class: "share-btn",
onclick: move |_| {
// navigator.clipboard.writeText(url)
console_log!("已复制: {url}");
},
"复制"
}
}
}
}
8. SEO 检查清单
发布前逐项检查:
#[derive(Debug)]
struct SeoCheck {
title: bool,
description: bool,
og_tags: bool,
canonical: bool,
structured_data: bool,
breadcrumb: bool,
image_alt: bool,
heading_structure: bool,
mobile_friendly: bool,
loading_speed: bool,
}
fn seo_audit(page: &str) -> SeoCheck {
// 检查各项 SEO 指标
SeoCheck {
title: true, // <title> 唯一且包含关键词
description: true, // meta description 存在且不重复
og_tags: true, // og:title, og:description, og:image, og:url 完备
canonical: true, // 有 rel=canonical 链接
structured_data: true, // JSON-LD 存在
breadcrumb: true, // 面包屑导航就位
image_alt: true, // 所有 img 有 alt 属性
heading_structure: true, // h1→h2→h3 层级合理
mobile_friendly: true, // 移动端适配
loading_speed: false, // 需要 Lighthouse 验证
}
}
9. 小结
- SPA 的 SEO 核心是动态管理
<head>元数据 use_headAPI 允许组件在渲染时动态设置 title、meta、link- Open Graph 标签控制社交分享预览
- JSON-LD 结构化数据增强搜索引擎展示
- 面包屑 = 用户体验 + SEO 信号
- 懒加载和尺寸预留改善 Core Web Vitals
- 建立 SEO 检查清单,确保每个页面都满足基本要求
- SSR 仍是 SPA SEO 的终极方案,结合预渲染可达到最佳效果