网站 SEO 优化
- W_Z_C
- 共 2155 字,阅读约 5 分钟
现如今搜索引擎优化(SEO)已经变成了一门学问,细节非常多,咱们这里只是简单的改造一下现有博客的页面,尽可能的贴近 Google 的官方指导建议,这样可能对网站的排名更有利。
1. 默认 SEO 配置
对于 SEO 优化,主要使用名称为 next-seo
的第三方插件,该插件可以提供一组可定制的 SEO 配置组件,我们可以将组件代码放到 _app.js 中,这样在所有页面中都可以生效,如果想要更改某个页面的 SEO 信息,还可以覆盖掉默认选项。
import { DefSEO } from '../../next-seo.config'import { DefaultSeo } from 'next-seo'
function MyApp({ Component, pageProps }) {
//...
return ( <> <DefaultSeo {...DefSEO} />
<Loading loading={loading} /> <main className={loading ? "hidden" : "block"}> <Component {...{ ...pageProps, loading }} /> </main> </>)}
具体的配置可以参考 next-seo
组件的文档,下面是我目前的配置信息,仅供参考:
import { formatISO, parseISO } from 'date-fns'
const websiteDomain = process.env.NEXT_PUBLIC_WEB_DOMAIN || 'https://127.0.0.1:3000'
export const DefSEO = {
defaultTitle: '没事造轮子',
titleTemplate: '%s - 没事造轮子',
robotsProps: {
noarchive: false, //不要显示缓存(付费文章之类的应该设置为 true)
nosnippet: false,
maxSnippet: -1,
maxImagePreview: 'large',
maxVideoPreview: -1,
notranslate: false,
noimageindex: false,
},
disableGooglebot: false,
twitter: {
cardType: 'summary_large_image',
site: '@WZC23559577',
handle: '@WZC23559577',
},
additionalMetaTags: []
}
除了上面的默认信息之外,在每个页面的头部还会添加一些 结构化数据头,目前的框架中使用了其中的几个:
- SocialProfileJsonLd,用于提供网站个人档案信息。
- LogoJsonLd,用于提供网站的 logo 和 URL。
这些都被统一添加到 _app.js 文件中,代表每个页面都会含有这些结构化数据。
<SocialProfileJsonLd
type='Person'
name='W_Z_C'
url={getURL()}
sameAs={[
'https://twitter.com/WZC23559577',
'https://www.youtube.com/channel/UCZFPn8CgiGP8EMB_LsF_NPg'
]}
/>
<LogoJsonLd
logo={getURL('logo.png', true)}
url={getURL()}
/>
代码中经常会使用 getURL 函数,该函数主要的目的是获取当前页面的 URL 信息,可以按照自己的喜好定义:
export function getURL(locale = '', slug = '', file = false) {
if (slug.length > 0 && slug[0] == '/') {
slug = slug.slice(1)
}
if(locale.toLowerCase() === 'zh-hans') {
locale = ''
}
if (slug.length < 1) {
return `${websiteDomain}/${locale}`
}
return `${websiteDomain}/${locale.length > 0 ? `${locale}/` : ''}${slug}${file ? '' : '/'}`
}
2. 主页 SEO 配置
主页配置只需要额外的增加一些元信息即可。
export function HomeLayout({ locale, trans, posts }) {
return (
<>
<CarouselJsonLd
ofType='course'
data={getCarousel(locale, trans)}
/>
<NextSeo {...getMetas(locale, trans)} />
//...
</>
)
}
CarouselJsonLd 组件可以提供一个轮询列表给 Google,可以很好的展示你网站的内容模块,但是最后是否展示取决于 Google 的算法,你只有推荐权,决定权在 Google。
NextSEO 和 DefaultSEO 类似,这里提供的信息可以覆盖掉 DefaultSEO 内相同种类的结果。
export function getMetas(locale, trans, title, desc, slug = '') {
return {
title: title || trans.SEO.Home,
description: desc || trans.SEO.Desc,
canonical: getURL(locale, slug),
openGraph: {
type: 'website',
locale: locale,
url: getURL(locale, slug),
site_name: trans.SEO.Site,
images: [{
url: getURL(locale, 'og-image.jpg', true),
width: 1920,
height: 1080,
alt: trans.SEO.Site
}],
}
}
}
这里生成的内容主要包含页面的标题和描述,以及 openGraph 相关信息。openGraph 是一个开源的数据交换协议,主要应用于 facebook 和 twitter 平台上分享展示的卡片内容,上面的组件会自动在页面中展开为下面的代码:
<meta property="og:url" content="https://meishizaolunzi.com/zh-Hans">
<meta property="og:type" content="website">
<meta property="og:image" content="https://meishizaolunzi.com/zh-Hans/og-image.jpg">
<meta property="og:image:alt" content="没事造轮子">
<meta property="og:image:width" content="1920">
<meta property="og:image:height" content="1080">
<meta property="og:locale" content="zh-Hans">
<meta property="og:site_name" content="没事造轮子">
<link rel="canonical" href="https://meishizaolunzi.com/zh-Hans">
很多平台在分享链接的时候,都会尝试读取网站的信息,openGraph 是比较流行的一种协议,主要显示网站的内容和主题,还会提供一张图片链接进行展示。
3. 其它页面设置
其它页面和主页的设置类似,虽然在结构化数据的表达上略有差异,但是整体上是一致的,所以没啥好将的,这里再用 post 页面模板举个例子,供大家参考:
export function PostLayout({ locale, trans, prev, next, meta, content }) {
return (
<>
<NextSeo {...getPageMetas(locale, trans, meta)} />
<BreadcrumbJsonLd
itemListElements={[{
position: 1,
name: trans.SEO.Home,
item: getURL(locale),
}, {
position: 2,
name: meta.category,
item: getURL(locale, `category/${meta.categorySlug}`)
}, {
position: 3,
name: meta.title,
item: getURL(locale, meta.slug)
}]}
/>
<ArticleJsonLd
url={getURL(locale, meta.slug)}
title={meta.title}
images={[
getURL(locale, meta.featureImage, true)
]}
datePublished={formatISO(parseISO(meta.createdAt))}
dateModified={formatISO(parseISO(meta.updatedAt))}
authorName={meta.author}
description={meta.description}
/>
//...
</>
)
}
该页面新增了两个额外的结构化数据单元,BreadcrumbJsonLd 用来标记网页的面包屑导航,ArticleJsonLd 用来描述网页的文章信息。
4. sitemap.xml
网站地图是提交网站链接的必备手段,所以咱们也需要把这个文件搞出来。生成该文件的方法很多,你可以使用 Nextjs 的预渲染功能,在编译期间就生成出它的静态文件,好处就是 Google 爬虫在读取该文件的时候会很快,坏处是内容固定,每次编译才会生成,这种方法对我们现在的系统足够了,但是我不准备这么做,因为以后我可能会增加动态博文功能。大致的意思就是在不需要重新编译的情况下,增加新的博客页面,其实 Nextjs 目前已经提供了该功能,好像叫做增量生成,具体名字忘记了,原理很简单,就是当用户访问的页面没有预渲染过,Nextjs 可以提供动态渲染的能力,目前咱们是直接返回 404,你可以修改 fallback 和 revalidate 参数进行增量静态生成,具体的方法可以参考 Nextjs 的文档。
目前咱们的系统还没有启用这个功能,主要原因是页面数量很少,完全没有必要。生成 sitemap 的另一个方法,就是使用动态页面,每次请求都重新生成当前访问的页面,我准备使用这个方法,如果以后开启增量生成功能,我们只需要将新的文档数据添加到数据库即可,而 sitemap 页面会自动追究新的链接数据。
下面是实现的具体过程,首先在 pages 文件夹下建立 sitemap.xml/index.js
文件,内容如下:
export default function SitemapXML() {
return null
}
export async function getServerSideProps({res}) {
return {props: {}}
}
因为是动态生成页面,所以将 SitemapXML 页面组件返回空,最终的渲染结果直接使用服务端的对象 res 进行生成。
sitemap 的格式基本固定,就是标准的 XML 文件,文件内部放置网站的所有链接,下面是页面的生成代码:
export async function getServerSideProps({res}) {
const data = await getSitemap()
const transformedData = [{
changefreq: 'weekly',
priority: 1.0,
slug: 'about',
}, {
changefreq: 'weekly',
priority: 1.0,
slug: 'download',
}].concat(data).map(page => {
if(Array.isArray(page)) {
if(page[1].length < 1) {
return null
}
const loc = getURL('', escapeXml(page[0]))
const lastmod = page[1][0].lastmod ? formatISO(parseISO(page[1][0].lastmod)) : undefined
const priority = page[1][0].priority
const changefreq = page[1][0].changefreq
const links = page[1].map(v => ({i18n: v.i18n, link: escapeXml(getURL(v.i18n, v.slug))}))
return {
loc, lastmod, priority, changefreq, links
}
} else {
return {
loc: getURL(escapeXml(page.slug)), //需要转义
lastmod: page.lastmod ? formatISO(parseISO(page.lastmod)) : undefined,
priority: page.priority,
changefreq: page.changefreq,
}
}
}).filter(v => v ? true : false)
const buildSitemapXml = (fields) => {
const content = fields.map(fieldData => {
const field = Object.entries(fieldData).map(([key, value]) => {
if(!value)
return
else if(key === 'links') {
return value.map(x => {
return `<xhtml:link rel="alternate" hreflang="${x.i18n}" href="${x.link}" />`
}).join('')
}
return `<${key}>${value}</${key}>`
})
return `<url>${field.join('')}</url>\n`
}).join('')
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">\n${content}</urlset>`
}
//生成 sitemap
const sitemapContent = buildSitemapXml(transformedData)
//返回
res.setHeader('Content-Type', 'text/xml')
res.write(sitemapContent)
res.end()
return {props: {}}
}
代码逻辑比较简单,就是按照一定的格式拼凑 XML 文件而已。getSitemap 函数用来从数据库中获取所有的页面链接:
export async function getSitemap() {
const db = getDB()
if (!db) {
console.error('获取数据库实例失败!')
return []
}
try {
const result = await db.$queryRaw`SELECT 'weekly' AS changefreq, i18n, 0.8 AS priority, ('category/' || categorySlug) AS slug, MAX(updatedAt) AS lastmod FROM posts GROUP BY categorySlug UNION ALL SELECT 'daily' AS changefreq, i18n, 0.7 AS priority, slug, updatedAt AS lastmod FROM posts ORDER BY priority DESC, lastmod DESC`
if(!result || result.length < 1) {
return []
}
let obj = {}
for(let i=0; i<result.length; i++) {
const line = result[i]
if(obj[line.slug]) {
obj[line.slug].push(line)
} else {
obj[line.slug] = [line]
}
}
return Object.entries(obj)
} catch(e) {
console.error('查询 sitemap 数据失败!', e)
return []
}
}
代码在获取所有数据行后进行了分组然后返回,接着 sitemap 生成逻辑会按照每组的链接信息生成支持多语言的 sitemap 文件,具体格式细节可以参考 Google 的说明文档。