加入 Open Graph、LD-JSON 等 SEO meta data - Modern Next.js Blog 系列 #17
- Published on
本文同步發佈於it 邦幫忙 2022 iThome 鐵人賽
TL;DR
這是「Modern Blog 30 天」系列第 17 篇文章,上一篇我們完成所有樣式切版了,這篇我們會使用 next-seo,為全站設定標題、描述文字、縮圖、Open Graph、LD-JSON,讓搜尋引擎知道每個頁面意義,做好 SEO!
結果截圖如下:
這篇修改的程式碼如下:
為全站及各頁面加入 meta data
經營部落格就是希望人們能來閱讀,能在搜尋引擎搜到你寫的文章。
而要做好搜尋引擎優化(Search Engine Optimization、SEO),除了內容好、樣式好看,我們也需要做些設定,讓搜尋引擎的爬蟲知道每個頁面在描述什麼,以及讓文章被貼到社群平台時,社群平台能知道該使用哪張縮圖。
方法是透過在各頁面 <head>
裡插入許多 <meta>
tag,標註頁面標題、概述、縮圖。
要在 Next.js 網站加入 meta tags,可以使用官方的 next/head
元件,在裡面插入 <meta>
來實作,可參考此處官方文件:next/head | Next.js。
另外一種方法是使用 next-seo 套件,它提供了包裝更完整的元件幫助渲染出所有必要的 meta tags,能簡化我們需要自己加的程式碼行數。
安裝 next-seo
已複製!pnpm add next-seo
為文章加入 socialImage 欄位,指定文章縮圖
我們希望能為每篇文章指定縮圖,在文章配貼到社群平台時會顯示的圖片。
圖片 socialImage 是一個 string,可以是 /public 資料夾內的圖片路徑,也可以是遠端圖片的網址。並且它不是必填的,如果寫文章時沒有指定,就會使用另一張全站共用的縮圖。
新增文章欄位需要修改 /contentlayer.config.ts
:
已複製!// ... export const Post = defineDocumentType(() => ({ name: "Post", filePathPattern: `content/posts/**/*.mdx`, contentType: "mdx", fields: { // ... date: { type: "date", required: true, }, // 新增 socialImage socialImage: { type: "string", }, }, // ... })); // ...
為文章指定 socialImage
接著就能在文章最前面區塊指定 socialImage 了,你可以挑一篇現成文章來加,或是像我這個 commit 一樣,新增一張圖片在 /public 裡面,並新增一篇文章來使用它當縮圖:
使用 next-seo 設定各頁面 meta data
完整改動可以看這支 commit:
新增 /src/configs/siteConfigs.ts
,並修改成你的網站想要的內容:
已複製!const fqdn = "https://nextjs-tailwind-contentlayer-blog-starter.vercel.app"; const logoPath = "/logo.png"; const bannerPath = "/og-image.png"; export const siteConfigs = { title: "Next.js Tailwind Contentlayer Blog Starter", titleShort: "Next Blog", description: "Blog starter template with modern frontend technologies like Next.js, Tailwind CSS, Contentlayer, i18Next", author: "Tony Stark", fqdn: fqdn, logoPath: logoPath, logoUrl: fqdn + logoPath, bannerPath: bannerPath, bannerUrl: fqdn + bannerPath, twitterID: "@EasonChang_me", datePublished: "2022-09-01", };
新增 /src/lib/getPostOGImage.ts
:
已複製!import { siteConfigs } from "@/configs/siteConfigs"; export const getPostOGImage = (socialImage: string | null): string => { if (!socialImage) { return siteConfigs.bannerUrl; } if (socialImage.startsWith("http")) { return socialImage; } return siteConfigs.fqdn + socialImage; };
修改 /src/pages/_app.tsx
:
已複製!import "@/styles/globals.css"; import "@/styles/prism-dracula.css"; import "@/styles/prism-plus.css"; import type { AppProps } from "next/app"; import { DefaultSeo } from "next-seo"; import { ThemeProvider } from "next-themes"; import LayoutWrapper from "@/components/LayoutWrapper"; import { siteConfigs } from "@/configs/siteConfigs"; function MyApp({ Component, pageProps }: AppProps) { return ( <ThemeProvider attribute="class"> <DefaultSeo titleTemplate={`%s | ${siteConfigs.titleShort}`} defaultTitle={siteConfigs.title} description={siteConfigs.description} canonical={siteConfigs.fqdn} openGraph={{ title: siteConfigs.title, description: siteConfigs.description, url: siteConfigs.fqdn, images: [ { url: siteConfigs.bannerUrl, }, ], site_name: siteConfigs.title, type: "website", }} twitter={{ handle: siteConfigs.twitterID, site: siteConfigs.twitterID, cardType: "summary_large_image", }} additionalMetaTags={[ { name: "viewport", content: "width=device-width, initial-scale=1", }, ]} additionalLinkTags={[ { rel: "icon", href: siteConfigs.logoPath, }, ]} /> <LayoutWrapper> <Component {...pageProps} /> </LayoutWrapper> </ThemeProvider> ); } export default MyApp;
修改 /src/pages/index.tsx
:
已複製!import type { NextPage } from "next"; import { GetStaticProps } from "next"; import { ArticleJsonLd } from "next-seo"; import PostList, { PostForPostList } from "@/components/PostList"; import { siteConfigs } from "@/configs/siteConfigs"; import { allPostsNewToOld } from "@/lib/contentLayerAdapter"; type PostForIndexPage = PostForPostList; type Props = { posts: PostForIndexPage[]; }; export const getStaticProps: GetStaticProps<Props> = () => { const posts = allPostsNewToOld.map((post) => ({ slug: post.slug, date: post.date, title: post.title, description: post.description, path: post.path, })) as PostForIndexPage[]; return { props: { posts } }; }; const Home: NextPage<Props> = ({ posts }) => { return ( <> <ArticleJsonLd type="Blog" url={siteConfigs.fqdn} title={siteConfigs.title} images={[siteConfigs.bannerUrl]} datePublished={siteConfigs.datePublished} authorName={siteConfigs.author} description={siteConfigs.description} /> <div className="prose my-12 space-y-2 transition-colors dark:prose-dark md:prose-lg md:space-y-5"> <h1 className="text-center sm:text-left">Hey,I am Iron Man ?</h1> <p>我是 Tony Stark,不是 Stank!</p> <p>老子很有錢,拯救過很多次世界。</p> <p>我討厭外星人、紫色的東西、和紫色外星人。</p> </div> <div className="my-4 divide-y divide-gray-200 transition-colors dark:divide-gray-700"> <div className="prose prose-lg my-8 dark:prose-dark"> <h2>最新文章</h2> </div> <PostList posts={posts} /> </div> </> ); }; export default Home;
修改 /src/pages/posts/[slug].tsx
:
已複製!import type { GetStaticPaths, GetStaticProps, NextPage } from "next"; import { useMDXComponent } from "next-contentlayer/hooks"; import { ArticleJsonLd, NextSeo } from "next-seo"; import PostLayout, { PostForPostLayout, RelatedPostForPostLayout, } from "@/components/PostLayout"; import { siteConfigs } from "@/configs/siteConfigs"; import { allPosts, allPostsNewToOld } from "@/lib/contentLayerAdapter"; import { getPostOGImage } from "@/lib/getPostOGImage"; import mdxComponents from "@/lib/mdxComponents"; type PostForPostPage = PostForPostLayout & { title: string; description: string; date: string; path: string; socialImage: string | null; body: { code: string; }; }; type Props = { post: PostForPostPage; prevPost: RelatedPostForPostLayout; nextPost: RelatedPostForPostLayout; }; export const getStaticPaths: GetStaticPaths = () => { const paths = allPosts.map((post) => post.path); return { paths, fallback: false, }; }; export const getStaticProps: GetStaticProps<Props> = ({ params }) => { const postIndex = allPostsNewToOld.findIndex( (post) => post.slug === params?.slug ); if (postIndex === -1) { return { notFound: true, }; } const prevFull = allPostsNewToOld[postIndex + 1] || null; const prevPost: RelatedPostForPostLayout = prevFull ? { title: prevFull.title, path: prevFull.path } : null; const nextFull = allPostsNewToOld[postIndex - 1] || null; const nextPost: RelatedPostForPostLayout = nextFull ? { title: nextFull.title, path: nextFull.path } : null; const postFull = allPostsNewToOld[postIndex]; const post: PostForPostPage = { title: postFull.title, date: postFull.date, description: postFull.description, path: postFull.path, socialImage: postFull.socialImage || null, body: { code: postFull.body.code, }, }; if (!post) { return { notFound: true, }; } return { props: { post, prevPost, nextPost, }, }; }; const PostPage: NextPage<Props> = ({ post, prevPost, nextPost }) => { const { description, title, date, path, socialImage, body: { code }, } = post; const url = siteConfigs.fqdn + path; const ogImage = getPostOGImage(socialImage); const MDXContent = useMDXComponent(code); return ( <> <NextSeo title={title} description={description} canonical={url} openGraph={{ title: title, description: description, url: url, images: [ { url: ogImage, }, ], type: "article", article: { publishedTime: date, modifiedTime: date, }, }} /> <ArticleJsonLd url={url} title={title} images={[ogImage]} datePublished={date} dateModified={date} authorName={siteConfigs.author} description={description} /> <PostLayout post={post} prevPost={prevPost} nextPost={nextPost}> <MDXContent components={mdxComponents} /> </PostLayout> </> ); }; export default PostPage;
新增網站 Logo 圖片,放在 /public/logo.png
。
新增網站預設 socialImage,放在 /public/og-image.png
。
成果
完成了!使用 pnpm dev
並進入首頁和文章頁面,打開 F12 查看原始碼 <head>
裡面內容,就會看到多出很多 meta data 了!
可以安裝這套 Chrome 瀏覽器 extension 來更方便查看每個頁面的 meta data:META SEO inspector - Chrome 線上應用程式商店
http://localhost:3000/posts/post-with-code
結果截圖如下:
這篇修改的程式碼如下:
References
- garmeeh/next-seo: Next SEO is a plug in that makes managing your SEO easier in Next.js projects.
- META SEO inspector - Chrome 線上應用程式商店
- next-seo 初體驗
- Free Render Image on Unsplash
下一篇
下一篇我們繼續處理 SEO,來加入 sitemap!