没事造轮子没事造轮子

没事造轮子

网站 SEO 优化

  • W_Z_C
  • 阅读约 5 分钟
网站 SEO 优化

现如今搜索引擎优化(SEO)已经变成了一门学问,细节非常多,咱们这里只是简单的改造一下现有博客的页面,尽可能的贴近 Google 的官方指导建议,这样可能对网站的排名更有利。

1. 默认 SEO 配置

对于 SEO 优化,主要使用名称为 next-seo 的第三方插件,该插件可以提供一组可定制的 SEO 配置组件,我们可以将组件代码放到 _app.js 中,这样在所有页面中都可以生效,如果想要更改某个页面的 SEO 信息,还可以覆盖掉默认选项。

_app.js
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 组件的文档,下面是我目前的配置信息,仅供参考:

next-seo.config.js
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 文件中,代表每个页面都会含有这些结构化数据。

_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 信息,可以按照自己的喜好定义:

next-seo.config.js
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 配置

主页配置只需要额外的增加一些元信息即可。

layouts/home.js
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 内相同种类的结果。

next-seo.config.js
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 页面模板举个例子,供大家参考:

layouts/post.js
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 文件,内容如下:

pages/sitemap.xml/index.js
export default function SitemapXML() {
    return null
}

export async function getServerSideProps({res}) {

    return {props: {}}
}

因为是动态生成页面,所以将 SitemapXML 页面组件返回空,最终的渲染结果直接使用服务端的对象 res 进行生成。

sitemap 的格式基本固定,就是标准的 XML 文件,文件内部放置网站的所有链接,下面是页面的生成代码:

pages/sitemap.xml/index.js
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 函数用来从数据库中获取所有的页面链接:

db.mjs

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 的说明文档