folder_open

使用 Next.js 手把手建立自己的部落格

arrow_right
article

SEO 搜尋引擎最佳化

SEO 搜尋引擎最佳化

Sitemap

#

Sitemap 是提供搜尋引擎讀取站內頁面清單的常見方式,可以加速搜尋引擎幫我們的網站建立索引,而網站主需要做的事情就是在根目錄提供 檔案,檔案內容也需遵循 Sitemap 規範。

在 Next.js 的專案中,我們可以手動建立 、手動填入 xml,並以靜態檔案的方式部署。然而,每當我們的部落格新增或刪除文章時,就必須要手動更新 ,這顯然會造成管理上的困擾。為了解決這個問題,我的做法是取巧地建立一個空白頁面,但是在 裡渲染並輸出

personal-blog/pages/generateSitemap.js
import { writeToPublic } from '../utils/output'
import { getPosts } from '../utils/post'
import { generateSiteMapInXML } from '../utils/seo'
export default () => {}
export const getStaticProps = async () => {
const posts = getPosts()
const sitemap = generateSiteMapInXML(posts)
writeToPublic('sitemap.xml', sitemap)
return {
props: {},
}
}

此處獨立出 Utility function ,負責把檔案寫入 資料夾:

personal-blog/utils/output.js
import fs from 'fs'
import path from 'path'
export const writeToPublic = (relativeFilePath, fileContent) => {
const absoluteFilePath = path.join(process.cwd(), 'public', relativeFilePath)
fs.writeFileSync(absoluteFilePath, fileContent)
}

Sitemap 檔案內容的渲染過程也獨立於 utility function

personal-blog/utils/seo.js
export const generateSiteMapInXML = (posts) => {
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${posts
.map(
(post) => `
<url>
<loc>{`https://example.com/blog/${post.slug}`}</loc>
<lastmod>${post.lastModifiedTime}</lastmod>
</url>`
)
.join('')}
</urlset>`
}

最後記得把這一份建置檔案列入 gitignore:

personal-blog/.gitignore
/public/sitemap.xml

伺服器端渲染 (SSR, Server Side Rendering)

#

要讓搜尋引擎能夠理解我們的網站內容,除了使用 Sitemap 編列頁面索引以外,每一個獨立頁面也都要能夠支援伺服器端渲染(SSR),這樣子爬蟲程式讀取頁面時才能夠拿到結構化的 Html 及頁面內容,而不是拿到需要被 Evaluate 的 Javascript。不過,Next.js 原生就已經支援 SSR 了,我們不必做任何處理就能享有 SSR 的效果,SSG 的過程也可以直接使用 SSR 產生的內容。

雖然對於爬蟲程式而言我們不用做任何努力,但是部落格的佈景主題、客製化元件等都是延遲載入 CSS,對於真人閱讀者而言,將會先看到 SSR 產生的 HTML,等到 CSS 被載入之後才套用設計過的佈景主題,這段載入 CSS 的時間差會造成閱讀者看到頁面閃爍、抖動、跳躍而影響閱讀體驗,為了讀者的眼睛著想,我的解法是把 CSS 納入 SSR 的過程當中。

由於我的 Design System 和各種調整樣式的處理方式都是透過 來完成,所以只要替 styled-components 設定 SSR 即可,參考 How to Set up Styled-Components with SSR in NextJS (Typescript)open_in_new 一文之後,兩個步驟就能搞定:

  1. 調整 Next Config

    personal-blog/next.config.mjs
    const nextConfig = {
    // ...
    compiler: {
    styledComponents: true,
    },
    }
    export default withMDX(nextConfig)
  2. 更新

    personal-blog/pages/_document.js
    import Document, { Head, Html, Main, NextScript } from 'next/document'
    import { ServerStyleSheet } from 'styled-components'
    class MyDocument extends Document {
    static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage
    try {
    ctx.renderPage = () =>
    originalRenderPage({
    enhanceApp: (App) => (props) =>
    sheet.collectStyles(<App {...props} />), //gets the styles from all the components inside <App>
    })
    const initialProps = await Document.getInitialProps(ctx)
    return {
    ...initialProps,
    styles: (
    <>
    {initialProps.styles}
    {sheet.getStyleElement()}
    </>
    ),
    }
    } finally {
    sheet.seal()
    }
    }
    render() {
    return (
    <Html>
    <Head />
    <body>
    <Main />
    <NextScript />
    </body>
    </Html>
    )
    }
    }
    export default MyDocument

JSON-LD

#

JSON-LD 是一種結構化語法,用來描述網站的型態及內容,搜尋引擎也會針對帶有 JSON-LD 的網頁呈現特化的搜尋結果排版。使用方式其實就是在網頁裡插入 標籤,而內容只要依照 Schema.orgopen_in_new 的規範填寫即可。

我的實作是撰寫專用的 SEO 元件,並且在文章頁面插入該元件:

personal-blog/components/utility/SEO.js
import Head from 'next/head'
import { generateJsonLdForPost } from '../../utils/seo'
export default ({ post }) => {
return (
<Head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: generateJsonLdForPost(post),
}}
key="product-jsonld"
/>
</Head>
)
}

並且搭配 utility function 來產生 JSON-LD 內容:

personal-blog/utils/seo.js
export const generateJsonLdForPost = (post) => {
const { HOST, SITE_NAME, AUTHOR_NAME } = getConfig()
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.frontMatter.title,
genre: ['SEO', 'JSON-LD'],
keywords: post.frontMatter?.keywords || [],
url: getUrl(HOST, post),
datePublished: post.frontMatter.publishDate,
dateCreated: post.frontMatter.publishDate,
dateModified: post.lastModifiedTime,
articleBody: post.content,
publisher: {
'@type': 'Organization',
name: SITE_NAME,
url: HOST,
logo: {
'@type': 'ImageObject',
url: `${HOST}/site/favicon.svg`,
},
},
author: {
'@type': 'Person',
name: AUTHOR_NAME,
url: HOST,
},
creator: {
'@type': 'Person',
name: AUTHOR_NAME,
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `${HOST}/blog`,
},
}
if (post.frontMatter.hero) {
jsonLd.image = `${HOST}${post.frontMatter.hero.src}`
}
if (post.frontMatter.excerpt) {
jsonLd.description = post.frontMatter.excerpt
}
return JSON.stringify(jsonLd)
}

測試 JSON-LD

#

如果想要驗證自己的 JSON-LD 是否正確,可以參閱 Google 的文件「測試結構化資料open_in_new」。