From 09bf3295441f81956539dfc53304cc2a8e4820d5 Mon Sep 17 00:00:00 2001 From: TradeMate Dev Date: Sat, 11 Jul 2026 17:23:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(seo):=20=E7=BB=93=E6=9E=84=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE/=E6=AF=8F=E9=A1=B5=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE/=E5=88=97=E8=A1=A8=E9=A1=B5metadata/hreflang=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3/OG=E5=88=86=E4=BA=AB=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 详情页注入 Article+BreadcrumbList JSON-LD,工具页 SoftwareApplication - 课程/技能/文章/练习 动态 generateMetadata(canonical/OG) - 新增 skills/contents/practices/prompts/models 列表页 metadata - 修正 hreflang(删除不存在的/en),用 next/og 生成 OG 分享图 --- docs/progress/current.md | 13 +++++ frontend/src/app/contents/[id]/page.tsx | 60 ++++++++++++++++++++-- frontend/src/app/contents/layout.tsx | 11 ++++ frontend/src/app/courses/[id]/page.tsx | 64 ++++++++++++++++++++++-- frontend/src/app/layout.tsx | 10 ---- frontend/src/app/models/layout.tsx | 11 ++++ frontend/src/app/opengraph-image.tsx | 37 ++++++++++++++ frontend/src/app/practices/[id]/page.tsx | 53 ++++++++++++++++++-- frontend/src/app/practices/layout.tsx | 11 ++++ frontend/src/app/prompts/layout.tsx | 11 ++++ frontend/src/app/skills/[id]/page.tsx | 53 ++++++++++++++++++-- frontend/src/app/skills/layout.tsx | 11 ++++ frontend/src/app/tools/[slug]/page.tsx | 13 ++++- frontend/src/lib/seo.ts | 59 ++++++++++++++++++++++ 14 files changed, 389 insertions(+), 28 deletions(-) create mode 100644 frontend/src/app/contents/layout.tsx create mode 100644 frontend/src/app/models/layout.tsx create mode 100644 frontend/src/app/opengraph-image.tsx create mode 100644 frontend/src/app/practices/layout.tsx create mode 100644 frontend/src/app/prompts/layout.tsx create mode 100644 frontend/src/app/skills/layout.tsx create mode 100644 frontend/src/lib/seo.ts diff --git a/docs/progress/current.md b/docs/progress/current.md index bf8019f..52067ee 100644 --- a/docs/progress/current.md +++ b/docs/progress/current.md @@ -48,6 +48,19 @@ > **说明:** 后端 `orders` / `payment` 模块代码保留(取证后可复用),但前端已不再调用付费购买流程。 +### SEO 基础优化 — v1.2.0 + +| 项目 | 改动 | 说明 | +|------|------|------| +| 结构化数据 | 详情页注入 JSON-LD | 课程/技能/文章/练习:`Article` + `BreadcrumbList`;工具:`SoftwareApplication`(含免费 Offer) | +| 每页元数据 | 详情页 `generateMetadata` | 课程/技能/文章/练习动态生成 unique title/description + canonical + OG | +| 列表页元数据 | 新增 `skills/contents/practices/prompts/models` 的 `layout.tsx` | 补齐各频道独立 title/description/canonical,消除与首页重复 | +| hreflang | 修正 `layout.tsx` | 删除不存在的 `/en` 备用语言,仅保留 `zh-CN` 自引用 | +| OG 分享图 | 新增 `app/opengraph-image.tsx` | 用 `next/og` 在构建期生成 1200x630 分享图(Latin 文案,规避 CJK 字体缺失),替换原缺失的 `/images/og-image.png` | +| 工具页 | 增强 `SoftwareApplication` JSON-LD | 补充 publisher / offers(免费) / operatingSystem | + +> **已知最大盲点(待办):** `courses/[id]`、`skills/[id]`、`contents/[id]`、`practices/[id]` 的**正文内容仍在客户端 `useEffect` 拉取**,静态 HTML 中无正文,百度等弱 JS 渲染的搜索引擎抓不到实质内容(`tools/[slug]` 已是服务端渲染,正常)。需将详情页改为「服务端取数 → 作为 `initialData` 传入客户端组件」以把正文写入静态 HTML。 + --- ## 待办(下一阶段) diff --git a/frontend/src/app/contents/[id]/page.tsx b/frontend/src/app/contents/[id]/page.tsx index d32cf2a..480ebe4 100755 --- a/frontend/src/app/contents/[id]/page.tsx +++ b/frontend/src/app/contents/[id]/page.tsx @@ -1,4 +1,17 @@ -import { API_BASE } from '@/lib/config'; +import type { Metadata } from 'next'; +import { API_BASE, SITE_URL } from '@/lib/config'; +import { articleJsonLd, breadcrumbJsonLd, absoluteUrl } from '@/lib/seo'; +import ContentDetailClient from './client'; + +async function getContent(id: string) { + try { + const res = await fetch(`${API_BASE}/contents/${id}`, { next: { revalidate: 3600 } }); + if (!res.ok) return null; + return res.json(); + } catch { + return null; + } +} export async function generateStaticParams() { try { @@ -12,8 +25,45 @@ export async function generateStaticParams() { } } -import ContentDetailClient from './client'; - -export default function ContentDetailPage() { - return ; +export async function generateMetadata({ params }: { params: { id: string } }): Promise { + const content = await getContent(params.id); + if (!content) return { title: '文章未找到' }; + const title = content.title; + const description = content.summary || content.content?.slice(0, 120) || `${content.title} - 宇之然 AI 文章`; + const url = absoluteUrl(`/contents/${content.id}`); + return { + title, + description, + alternates: { canonical: `/contents/${content.id}` }, + openGraph: { type: 'article', title, description, url }, + }; +} + +export default async function ContentDetailPage({ params }: { params: { id: string } }) { + const content = await getContent(params.id); + const url = absoluteUrl(`/contents/${params.id}`); + const jsonLd = content + ? [ + articleJsonLd({ + headline: content.title, + description: content.summary || undefined, + url, + datePublished: content.publishedAt || content.createdAt, + }), + breadcrumbJsonLd([ + { name: '首页', url: SITE_URL }, + { name: '文章', url: absoluteUrl('/contents') }, + { name: content.title, url }, + ]), + ] + : null; + + return ( + <> + {jsonLd && ( +