继续分享 Next.js 14 SEO 最佳实践。上次主要介绍 SEO 基础知识,以及一般原则,这次重点放在 Next.js 14 上。我先武断暴言一句:Vercel 在 Next.js 14 上的整了个烂活儿,给开发者留下了很大的坑要填。
页面组件避免 'use client'
Next.js 14(也可能更早,待查)引入了 server
和 client
组件的区分。前者没有状态,渲染一次后就不会变化;后者则可以与用户互动,响应用户操作,并将变化体现在视图之中。
SEO 有一些必备信息,包括 title
, description
,需要通过 export const metadata
或 getMetadata
返回。它们都不支持 client
组件,所以,我们应该避免让页面组件(page.tsx
)成为 client
。
按照官方文档的说法,client
组件也会在服务器端进行渲染,然后发给客户端,所以比较合适的做法是:
- 组件化颗粒度可以细一些,以便将不需要状态的
server
组件和响应用户操作的client
组件进行隔离。 - 必要的数据,尤其是希望搜索引擎抓取并索引的数据,在页面组件里完成读取,然后传递给
client
组件,作为初始值,完成第一次 SSR。 getMetadata
时,可以使用React.cache
函数将结果缓存起来,重复使用,降低网络请求成本- 限制
client
的功能比较多,比如createContext
,所以工具函数文件不妨也分离得细一些,避免产生影响。
确保返回页面 metadata
SEO 需要一些信息来理解页面,并在搜索时呈现给用户,这些信息基本上都要通过 metadata
提交给搜索引擎,所以我们应该保证在 page.tsx
里都有返回合适的 metadata
。
一般来说,metadata
需要包含以下数据,后面的代码简单演示了动态获取并返回信息的方式:
- 页面 title
- 页面内容 description
- OpenGraph 信息
- Canonical link
- 如果有多语言,还应该有 alternate link
export async function generateMetadata(
{ params, searchParams }: PracticeDetailPageProps,
parent: any
): Promise<Metadata> {
// 读取路由参数
const courseId = params.courseId;
// 这里的加载函数可以提前 cache,以避免重复请求 API
const courseDetail =
await webApi.courseApi.fetchCourseDetail<CourseDetailType>(courseId);
const metadata: Metadata = {
title: courseDetail.title,
description: courseDetail.description,
alternates: {
canonical: `https://www.hackquest.io/practices/${courseId}`
},
openGraph: {
title: courseDetail.title,
description: courseDetail.description,
image: courseDetail.thumbnail,
},
};
return metadata;
}
增加组件颗粒度,创造条件使用 server
组件
因为静态内容更有利于 SEO,而 server
组件是生成静态内容的最好方式,所以我们应该尽量多使用 server
组件。
问题在于 client
组件具有传染性,当某个组件内部使用状态的时候,它就必须是 client
组件,所以我们应该尝试让组件颗粒化的程度更细,仅在需要交互的组件里使用状态。
使用 <Link>
而不是 router.push(newPage)
跳转
搜索引擎的爬虫可以沿着超链接爬取一个又一个页面,而它们并不知道某个 <div>
上绑定了跳转事件,所以应该使用 <Link>
帮助搜索引擎索引整个网站。
很多时候,为满足埋点等需求,我们会使用 onClick
事件,没关系,因为事件会先于链接跳转触发,所以我们只需要正常使用埋点功能即可。
全站使用统一的通用导航
使用全站通用的顶部导航和底部导航是提升 SEO 和网页收录的好办法。
通用导航里面可以包含大量站内链接,方便搜索引擎爬虫遍历整个网站,所以我们应该在通用导航里大量使用 <Link>
,让爬虫可以顺藤摸瓜,找到我们网站里的全部内容。当然,从用户角度的来说,通用导航可能并不是每次打开页面的主要目的,所以,通过 CSS 控制通用导航的显示状态,让爬虫可以按图索骥,但是不影响普通用户阅读网页,是我们应该做的。
所以,我们应该让整个网站使用统一的导航内容,只调整显示状态,而不是每个页面显示不同的导航。
分离工具函数和依赖,避免污染
影响 client
组件的因素很多,比如各种 hooks,createContext
,等。如果我们在 a
函数里使用了这些功能,然后在 b
里 import a
,会导致 b
也必须是 use client
。这就要求我们尽量将引用的工具函数进行分离,比如单独写一个 utils.client.ts
用来存放客户端才需要使用的工具函数。
使用 [[...slug]]
处理 URL
如同前面所说,我们应该给所有资源配置好合适的 URL。比如我们有一些博客内容,需要建立博客页面,那么,我们就应该给翻页功能分配好独立的 URL,如 /p/${page}
,而不是把翻页状态保存在页面状态里。同样,如果我们支持按标签、分类筛选,就可以提供 /tag/${tag-slug}
这样的 URL。
此时,为了能更好的复用代码、复用组件,我们可以用可选通配符组件 [[...slug]]
来制作页面。
const Blog: React.FC<BlogProps> = async function ({
searchParams = {},
params: { slug = [] }
}) {
const limit = 12;
const minPage = Number(slug[1]) < 1 ? 1 : Number(slug[1]);
const page = slug[0] === 'p' ? minPage : 1;
// 处理加载分页后的博客数据
....
}
制作 robots.txt
与 sitemap.xml
制作 robots.txt
比较简单,很多时候手写一个放到 /public
目录里即可,所以我不再赘述。
制作 sitemap.xml
就复杂很多,因为网站可能会有很多页面,其中更有不少需要动态生成,所以我们一般需要创建 sitemap.ts
才能完成。部署的时候,构建脚本会自动调用这个文件,生成新的 sitemap.xml
。
详情请参考官方文档:Generating a sitemap using code (.js, .ts)
小结
如果你对 Next.js 或者 SEO 有什么问题和想法,欢迎留言讨论。
本站目前还在招商:本站广告 2024 年招商,欢迎感兴趣的老板私信洽谈。
系列文章
- 了解 SEO,常规要点
- Next.js 14 的 SEO(本文)
- Next.js 14 性能提升(TODO)
系列长文好更不好看,所以我决定以后都在 notion 上做汇总页,方便日后整体阅读。本文实际已经基本写完了,感兴趣的同学可以先去围观:https://meathill.notion.site/Next-js-14-SEO-90d31eee22ff4621af6620524b4b2773?pvs=4
欢迎吐槽,共同进步