Blog Cover Image

Inspire you to have New thinking, Walk out your unique Road.

從 Hugo 到 Next.js (SSG):重構個人網站並保住 30 萬歷史瀏覽與每日 50+ 穩定造訪的 SEO 成果

Posted on Jun 10, 2025

Google Analytics Screenshots

本文適用於使用 Next.js 開發網站或者希望維持或優化 SEO 表現的讀者。其 SEO 的知識跟優化核心理念都是相同的。

內容出於本人經驗,其 SEO 的知識與資訊請多加參考其他網站跟書籍,若有誤或問題歡迎寄信通知,感謝您!

文章目錄 Table of Contents

前言

設計與打造自己的個人網站,一直是我長久以來的夢想。

在軟體技術還不夠熟練的時期,我曾經使用 WordPress 架設個人網站。不過我一直嚮往能更自由地組合與呈現網頁內容、設計前端介面,因此開始尋找更能實現這些想法的技術。

2019 年,在一位前同事的推薦下,我接觸並使用了當時非常熱門的 Jamstack 框架 —— Hugo。這是一個靜態網站生成器,具備快速架站、良好 SEO 表現等優勢,讓我能順利打造出自己的個人網站。從原本在 WordPress 上僅有 1 萬人次的閱讀數,到如今累積超過 30 萬次瀏覽,每日平均也有超過 50 位訪客,Hugo 一路陪伴我走到現在。

不過,隨著我在職涯上從後端(主要使用 Python)轉向前端,開始學習 JavaScript,也逐漸意識到自己對 Hugo 背後使用的 Golang 並不熟悉,這在我想嘗試設計不同 UI 主題時成為一個不小的限制。

也因此,我重新踏上探索之路,希望能找到一種方式,一邊精進自己的前端技術,一邊實現更自由地設計與呈現個人網站 UI 的願望。

從 Hugo 搬運去 Next.js

還記得就讀研究所時,前端三大框架(React、Vue、Angular)都還沒出現。那時我們還是用最純粹的 HTML、CSS、JavaScript(或 jQuery),配合 MVC 架構和後端技術來建構網站。

大約 1-2 年前,我開始從後端轉向前端學習,初次接觸 Next.js 時,對於各種不同的渲染方式(CSR、SSR、SSG、ISR)完全不熟悉。尤其 CSR 與 SSR 的差異與衝突讓我非常困惑。相比純 CSR 的 React.js,Hugo 採用的是 SSG 模式,一開始就挑戰 Next.js 的我,面對複雜的概念與架構,學習曲線非常陡峭。儘管我多次想要用 Next.js 重構現有的 Hugo 網站,卻總是卡關而未能實現。

主要的阻礙有幾個:

  • 不熟悉前端開發,尤其對於不同渲染模式的理解不足,導致開發困難。
  • 缺乏 SEO 優化的經驗,擔心重構後無法達到 Hugo 原本出色的 SEO 效果。
  • URL 動態路徑的變更會影響既有的 Google 搜尋排名。

目前這個使用 Hugo 架設的部落格,累積了約 5 ~ 6 年的內容與流量,總瀏覽量已突破 30 萬,每天仍維持約 50+ 的訪客。其中許多文章也已經穩定出現在 Google 的搜尋結果中。

為了維持既有的 SEO 表現,一個關鍵就是必須保留原本的 URL 結構。否則一旦網址改變,就會出現 404 找不到頁面 的問題,或需額外設定轉址,也會影響搜尋排名。

Hugo 當初的路徑設計如下:

  • 中文主頁:/
  • 英文主頁:/en/
  • 中文文章:/<年>/<月>/<文章>
  • 英文文章:/en/<年>/<月>/<文章>

但在用 Next.js 重構時,為了支援多語系,我使用了動態路由設計 /[lang]/page.js,結果讓原本的 /2024/04/post 被變更為 /zh-TW/2024/04/post,直接改變 URL 結構,造成 Google 找不到舊文章,排名也受到影響。

直到後來我發現 Next.js 有個非常關鍵的功能叫做 Catch-all Segments,可以透過 /[...routes]/page.js 加上 generateStaticParams,自由定義動態路徑,不必受限於語言或特定格式。

例如:

  • 傳入路徑陣列 ['en', '2024', '04', 'post']/en/2024/04/post
  • 傳入 ['2023', '01', 'post']/2023/01/post

這樣我就可以保留原本文章的 URL 結構(如 /2024/04/post),讓搜尋引擎依然可以正確導引至文章頁面。

在解決這些問題後,我終於下定決心,從 Hugo 遷移到 Next.js,正式啟動網站重構計畫。

上線成果與後續觀察

重構後正式的上線時間為 2025/05/26,到現在 06/05 也過去快 2 週了,運用 Google Analytics (GA)Google Search Console 觀察了幾天,點閱數跟 SEO 都有維持原本的數值,直接用 Next.js 重構的結果算順利 (先前有使用 Nuxt.js 重構結果 SEO 直線下降的案例)。

Google Analytics

依據 Google Analytics 5/26 上線後每日的訪客與讀者量沒有直線下降。

Google Search Performance

在 Google Search Console 的 Performance 也是呈現一樣的成果

Google Search Indexing

在 Google Search Console 的 Indexing 有偵測出更多的有效頁面,其他一些未被 Indexed 的頁面可能是過去過期的或者之前亂寫的時候產出錯誤的路徑,未來會手動將他們註銷掉。

SEO 排名保持穩定、舊連結沒有 404 錯誤、而且 Google Robots 偵測出更多頁面並登入 Index,證明這次重構總算沒白費,也沒有失敗,讓我累積了不少關於 SEO 優化跟 Next.js 的實戰經驗。

對於 SEO 優化的知識也持續在學習跟加強,例如研究如何使用 Schema.org 跟 Lighthouse 進行 SEO 優化。當然我同時也重新檢視自己的網站內容吸不吸引人,需不需要做更改。

我的個人網站流量雖不像電商網站每日幾千人、幾萬人造訪,但至少不像先前有使用 Nuxt.js 重構且文章排名跟造訪人數直線下降的慘案發生。

接下來的幾點我想就維持/優化 SEO 的技巧跟大家分享幾個重要的點。

SEO 優化需要注意的點

我用的 Next.js 版本: 14.2.5

1. Metadata

Metadata 是為網頁定義資訊,讓 Google 爬蟲能了解頁面主題、語言、作者等,也能決定網站被分享時的呈現方式。

建議每一個希望被 Google 納入搜尋結果的頁面(例如部落格文章),都要設置專屬的 Metadata。以下是我的實作經驗:

  • 基本資訊:title、description、keywords(雖然影響力變小,仍建議放 5 個)。
  • Favicon:提高品牌辨識度的頁籤小圖示。
  • 額外資訊:如 generator、authors、creator。
  • Robots.txt 設定:告訴搜尋引擎哪些頁面要被收錄。
  • alternates:支援多語系網站時告知對應網址,務必確認 canonical 設定正確,錯誤的 canonical 連結會導致分享連結或者進入網站錯誤。
  • openGraph(OG)與 Twitter:定義分享時的標題、敘述與圖片。

這些 Metadata 能幫助搜尋引擎正確理解與索引你的網站內容,對 SEO 效果顯著。以下是我在 Next.js 中設定 Metadata 的範例。

const metadata = {
  // 基本資訊
  title: "分頁名稱 - 網站名稱", // iPhone - Apple (台灣)
  description: "描述",
  keywords: ["關鍵字1", "關鍵字2", "關鍵字3", "關鍵字4", "關鍵字5"],

  // 額外資訊
  generator: "Next.js 14.2.5",

  // Favicon
  icons: {
    icon: "/static/favicon.jpg",
    shortcut: "/static/favicon.jpg",
    apple: "/static/favicon.jpg",
  },
  applicationName: "網站名稱",
  referrer: "origin-when-cross-origin",

  authors: [
    { name: "Mina" },
    { name: "@Mina Influence", url: "https://minayu.site" },
  ],
  creator: "Mina",
  publisher: "Mina",

  // Safari或手機網站: 是否要偵測網頁內有無包含Email, Address, 電話格式
  formatDetection: {
    email: false,
    address: false,
    telephone: false,
  },
  // Robots.txt,告訴爬蟲機器人哪些網站需要被收錄、哪些不需要
  robots: {
    index: true,
    follow: true,
    nocache: false,
    googleBot: {
      index: true,
      follow: true,
      noimageindex: false,
      "max-video-preview": -1,
      "max-image-preview": "large",
      "max-snippet": -1,
    },
  },
  // 多語系的話,可以在這列出。canonical 是當前網站的網址。languages 則是列出相同內容但是不同語系的網址。
  // 要注意後面不要有底線,或者要親自檢查 Head 中的 canonical 網址是否正確,這會關乎你點擊分享按鈕時複製的網站是否正確。
  alternates: {
    canonical: "https://minayu.site/about",
    languages: {
      en: "https://minayu.site/en/about",
    },
  },

  // OG,當你分享文章到不同的平台時,他會怎麼被呈現
  openGraph: {
    title: "關於我 - 月亮水瓶座 @Mina 的網站",
    description: "紀錄這個網站的資訊、歷史及關於我的個人資料。",
    url: "https://minayu.site/about",
    type: "article",
    publishedTime: "",
    authors: ["Mina Yu"],
    siteName: "月亮水瓶座 @Mina 的網站",
    images: [
      {
        url: "https://<test-website>/static/img.jpg", // Must be an absolute URL
        width: 1200,
        height: 630,
      },
    ],
    locale: "zh-TW",
  },

  // Twitter
  twitter: {
    card: "summary",
    title: "關於我 - 月亮水瓶座 @Mina 的網站",
    description: "紀錄這個網站的資訊、歷史及關於我的個人資料。",
    siteId: "@MingJungYU",
    creator: "@MingJungYU",
    creatorId: "",
    images: {
      url: "https://<test-website>/static/img.jpg",
      alt: "@Mina Influence Image",
    },
  },
};

2. 需要 SEO 優化的頁面,確認是 SSG 或 SSR 渲染模式

前端有多種渲染方式:CSR、SSR、SSG、ISR、Hydration。這些術語若不是專攻前端,可能會比較陌生,但我們可以先略過細節,只記住這個重點:

需要做 SEO 的頁面,一定要使用 SSR 或 SSG。

如果你不熟悉這些模式,又只是想架設個人網站,建議使用像 Hugo 這種預設使用 SSG 的 Jamstack 框架 框架,找熱門、有社群支援的比較容易查資料。這樣就不用管什麼渲染模式,反正輸出都是靜態網頁。

若使用 Next.js,可以透過設定達成靜態輸出:

1. 設定 next.config.mjs

const nextConfig = {
  output: "export", // 啟用靜態網站輸出
  distDir: "out", // 輸出到 /out 資料夾
  // 這一行看你要不要加,有加的會產出架構為 /blog/index.html or /2024/05/11/post/index.html 的架構,當訪問 /blog/ 或 /blog,Github Page 都會指向 /blog/index.html。
  trailingSlash: true,
  // 這一行部署時一定要改成你的網域,才會指向正確的 CSS 跟 Javascript。
  assetPrefix: isDev ? "" : "<hostname>",
};

export default nextConfig;

執行 npm run build 就會在 /out/ 資料夾產出靜態網站。

2. 路徑產出注意事項

所有頁面的網址都必須事先產出,Next.js 會根據你的資料夾自動建立路徑。

如果有動態路徑(例如 [id] 或 [...routes]),需要自己用 generateStaticParams 函式在 page.js 或 layout.js 明確列出所有要產出的頁面,否則會變成 404

範例:

// 我的資料夾: /divination/[method]/layout.js

const METHOD = ["runes", "tarot", "lenormand"];

// 要在這個函式裡列出所有 [method] 的可能參數,有列出來的話,該網址才會被找到不會404
export async function generateStaticParams() {
  const params = [];

  METHODS.forEach((method) => {
    params.push({ method });
  });

  return params;
}

export default async function Layout({ children }) {
  return children;
}
// 變形動態路徑範例
// 我的資料夾: /[...routes]/layout.js

export async function generateStaticParams() {
  const allPosts = await getAllPosts();
  const posts = allPosts.map((post) => {
    // For loop 每個文章後,定義他的路徑
    return {
      routes: [post.lang, post.year, post.month,post.slug ];,
    };
  });

  // 實際傳出去的資料會是
  // [
  //    {routes: ['2024', '05', '11', 'my-first-post']},
  //    {routes: ['2024', '06', '11', 'my-second-post']},
  //    {routes: ['2024', '07', '02', 'my-third-post']}, ...
  // ]

  return [...posts];
}

3. 避免整頁 CSR

剛開始學 Next.js 可能會習慣整頁都加 'use client',導致整頁變成 CSR,失去 SEO 效果。正確做法是:

  • 保持整頁 SSR/SSG
  • 把需要使用 useState 等 hook 的元件(例如 Carousel)抽出成獨立檔案,加上 'use client',再 import 回主頁

以下是範例,假設我今天有一個 blog 頁面,只有 Carousel 旋轉木馬需要用到 CSR,我可以將 Carousel 元件抽出來寫在別的檔案,避免和其他元件放在一起使得整頁都套用 CSR 模式。

// carousel.js
"use client";
export function Carousel() {
  return <>{/* TODO: Carousel */}</>;
}
// page.js
// 這麼一來這一頁就可以定義SEO屬性了。
import { Carousel } from "@app/component/carousel";
export default function Page({ params: { lng } }) {
  return (
    <>
      <Header />
      <Navbar />
      <Carousel />
      <Posts />
      <Categories />
      <Footer />
    </>
  );
}

4. 如何確認成功產出 SSG?

  • 先成功使用指令 npm run build 產出靜態網頁
  • 用編輯器打開 /out/ 資料夾內的 HTML 文件
  • 如果你看到 <head> 內的 metadata 及 <body> 有完整內容,代表成功產出靜態網頁 (但要記得確認 head tag 裡的 metadata 跟你特別定義的資料是一樣的,不是去抓 layout.js 裡預設的值,有的時候 Next.js 會直接抓預設值或者直接用主頁的 metadata 代替。)
  • 若只看到一個空的 <div>,代表該頁使用了 CSR,SEO 會失效

3. 建議:網址路徑(route)盡量用英文

因為我搬過幾次部落格,有些文章是從 WordPress 匯出下來的 .md 檔案,沒設定英文網址 slug,就直接用中文檔名,導致網址變成經過編碼的亂碼。例如:

  • 網址看起來像是:/2024/05/11/我的第一篇文章
  • 實際網址會變成:/2024/05/11/%E6%88%91%E7%9A%84%E7%AC%AC%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0

雖然瀏覽器或框架會幫忙處理中文編碼,但我實際遇到很多編碼/解碼錯誤(瀏覽器、Next.js、Babel 出現衝突),最後決定把文章 slug 全部改成英文比較穩定。

此外,中文網址對 SEO 表現不好。建議把網址都改成英文的 slug,例如:

  • /2024/05/11/我的第一篇文章/2024/05/11/my-first-post
  • /blog/categories/旅行日記/blog/categories/travel-diary

當然,如果你用的框架能正確處理中文網址,也可以不用改。

4. sitemap 和 robots 設定可幫助 SEO

sitemap

sitemap 是網站地圖,讓搜尋引擎知道網站有哪些頁面、更新頻率與時間。

通常放在網站根目錄,如 public/sitemap.xml,內容像這樣:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc><lastmod>2025-06-03</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
</urlset>

用 Next.js 可以裝套件 next-sitemap 自動產生 sitemap:

1. 建立 next-sitemap.config.js 檔案,設定站台網址與路徑排除邏輯。

2. additionalPaths: 需要額外定義的路徑或想重新設定的路徑資訊

用 next-sitemap 套件自動產出的 sitemap 網址日期都是當下的時間,我目前還不知道怎麼針對不同的網站設定更新日期,查了官方文件可以選擇在 additionalPaths 新增路徑與資訊,重複的路徑會被覆蓋,不會有重複的問題,所以可以用這個方式依據文章重新設定文章路徑跟日期覆蓋原先的設定,如下

module.exports = {
  siteUrl: "https://<hostname>", // ← 請改成你的 GitHub Pages 網址
  generateRobotsTxt: true,
  outDir: "public",
  changefreq: "weekly",
  priority: 0.8,

  // 你希望哪些網址是被 exclude 排除,不要加進sitemap
  exclude: [
    "/blog/page/*",
    "/blog/category/*/page/*", // 可依實際路由調整
  ],

  additionalPaths: async (config) => {
    // 原先在layout.js我就有依據posts產出路徑,但因為lastmod我希望是呈現文章的發布日期,所以這邊我再依據所有的文章內容重新定義一次,用覆蓋的方式設定文章的最後更新日期。

    const posts = await getAllPosts();
    const postPaths = posts.map((post) => {
      const basePath = post.lang === "en" ? "/en" : ""; // 因為我的網址預設中文是沒有在最前面加語系
      return {
        // 需要直接用 / 串起路徑
        loc: `${basePath}/${post.year}/${post.month}/${post.slug}`,
        lastmod: post.date.toISOString(),
      };
    });

    return [...postPaths];
  },
};

3. package.json 加上 prebuild script

  "scripts": {
    "prebuild": "next-sitemap",
    "build": "next build",
  },

4. 跑 npm run build 就會自動產出 sitemap。

robot.txt

robots.txt 是給爬蟲看的設定檔,放在網站根目錄或 public/ 資料夾。可以手動寫:

User-agent: *
Allow: /

Host: https://example.com
Sitemap: https://example.com/sitemap.xml

如果你用 next-sitemap,也可以直接自動產出 robots.txt,只要設定:

generateRobotsTxt: true;

5. 部署設定:assetPrefix、basePath、trailingSlash、CNAME、.nojekyll

部署網站時,網址通常會不同於本地開發的 http://localhost:3000,可能會變成自己的網域,或有子路徑如 /staging/,需要特別處理。

1. CNAME

若使用自己的網域,請在 public 資料夾中新增 CNAME 檔案(無副檔名),內容為你的網域。

2. .nojekyll

Next.js 的靜態資源會放在 _next/ 目錄。若用 GitHub Pages 部署,預設的 Jekyll 會忽略以 _ 開頭的資料夾,導致無法載入資源。請在 public 資料夾建立 .nojekyll 檔案(無內容)。

3. Next.js 設定

// next.config.mjs

const nextConfig = {
  output: "export",
  distDir: "out",
  // Next.js 一般不會去做 最後面的 slash / 轉址
  // 你會發現 /blog 會找到網頁, /blog/ 會 404
  // 仔細檢查如果沒有設定這個trailingSlash,你會發現只會產出 blog.js 檔案,這個時候路徑必須精準指向 /blog。加上這個設定後檔案會變成 /blog/index.html,部署到 Github Page 上之後,Github Page 會自動將 /blog 轉址至 /blog/,所以兩者都找得到網站。
  trailingSlash: true,
  // 當部署到Github時,你需要設定 那些 JS/Style檔案 從原本的 localhost ("") 指向你的網域 https://<hostname>/,才會找到正確的路徑。
  assetPrefix: isDev ? "" : "https://<hostname>/",
  // 如果你的網站有前綴,像是 /staging/ 或者架設在其他repo上,basePath需要改為前綴。
  // basePath: "/staging",
};
export default nextConfig;

如果遇到圖片、CSS、JS 路徑錯誤,請確認 assetPrefix 的設定與 .nojekyll 是否正確。

6. 部署後的 SEO 工具:GA 與 Search Console

網站上線後,可用以下工具監測訪問與 SEO 狀況:

Google Analytics (GA)

到 GA 建立追蹤碼,將以下程式碼加入 Next.js 頁面的 <head>,並將 G-XXXXXXXXXX 換成你的 GA 碼:

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        {/* GA4 Script */}
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
          strategy="afterInteractive"
        />
        <Script id="ga4-init" strategy="afterInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', 'G-XXXXXXXXXX', {
              page_path: window.location.pathname,
            });
          `}
        </Script>
      </head>
      <body>{children}</body>
    </html>
  );
}

Google Search Console

登入後將你的 Sitemap 網址提交給 Google,讓搜尋引擎能正確索引你的網站。也可以透過這個網站去檢查額外的 SEO 優化圖表。

最後

希望這些內容能幫助你順利部署網站與設定 SEO。我並非前端或 SEO 專家,只是分享我的個人經驗,希望對你有幫助!