iT邦幫忙

2022 iThome 鐵人賽

DAY 29
0
Modern Web

從零開始打造炫砲個人部落格,使用 Next.js、ContentLayer、i18next 等現代技術系列 第 29

在 Next.js Contentlayer blog 實作舊路徑轉址 - Modern Next.js Blog 系列 #29

這是本系列最後一篇實作文,最後來實作一個微小但重要的功能:舊路徑轉址!

這篇修改的程式碼如下:
https://github.com/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day28-i18next...day29-old-post-path-redirect

我的個人網站裡也有此系列的好讀版,程式碼更易讀、也支援深色模式和側邊目錄,歡迎前往閱讀!


Next.js Contentlayer blog 舊路徑轉址

部落格經營一段時間後,你可能因為各種原因,想要調整文章網址,例如:

https://easonchang.com/2016/12/13/grid-note
轉址到 https://easonchang.com/posts/grid-note

在 Next.js 要實作轉址(redirect)有兩種方式:

  1. 在 next.config.js 羅列出所有轉址規則,參考官方文件「Data Fetching: getStaticProps | Next.js
  2. 在 getStaticProps 或 getServerSideProps 裡根據自訂條件,回傳 redirect,參考官方文件「Data Fetching: getStaticProps | Next.js

這裡我們希望轉址規則是每篇文章都能自訂,寫在文章 .mdx 檔案裡面,所以要搭配 Contentlayer 分析所有文章設定後,才能知道所有轉址規則。

我在嘗試後發現方法 1 行不通,因為沒辦法在 next.config.js 裡 import Contentlayer 提供的 allPosts。

所以我們這篇文章會採用方法 2,在 getStaticProps 裡決定是否 redirect。

實作舊路徑轉址

首先修改 contentlayer.config.ts,在 Post 新增 redirectFrom 屬性,這樣就能在每篇文章 .mdx 裡設定各自的轉址規則:

// ...

export const Post = defineDocumentType(() => ({
  // ...
  fields: {
    // ...
    // 新增 redirectFrom
    redirectFrom: {
      type: 'list',
      of: { type: 'string' },
    },
  },
  // ...
}));

// ...

新增 src/lib/getAllRedirects.ts 來取得所有需轉址的路徑:

import { allPosts } from '@/lib/contentLayerAdapter';
import { unifyPath } from '@/lib/unifyPath';

export type Redirect = {
  source: string;
  destination: string;
  permanent: boolean;
};

export const getAllRedirects = () => {
  const redirects: Redirect[] = [];

  allPosts.forEach((post) => {
    const allRedirectFrom =
      post.redirectFrom?.map((from) => unifyPath(from)) || [];
    allRedirectFrom.forEach((from) => {
      redirects.push({
        source: from,
        destination: post.path,
        permanent: false,
      });
    });
  });

  return redirects;
};

export const allRedirects = getAllRedirects();

新增 src/lib/unifyPath.ts 統一轉址 URL 格式:

// Discard leading and trailing slashes
export const unifyPath = (path: string): string => {
  return '/' + path.replace(/\/$/, '').replace(/^\//, '');
};

新增 src/lib/stringifyCatchAllDynamicRoute.ts 將 route 轉成 string 方便後續處理:

export const stringifyCatchAllDynamicRoute = (
  route: string | string[] | undefined
): string => {
  if (!route) return '';
  if (Array.isArray(route)) return route.join('/');
  return route;
};

最後修改所有用到動態參數 Dynamic Routes 的 pages,目前只有文章內頁 [slug].tsx 需要修改。

修改 src/pages/posts/[slug].tsx

// ...
import { allRedirects } from '@/lib/getAllRedirects';
import { unifyPath } from '@/lib/unifyPath';
// ...

export const getStaticProps: GetStaticProps<Props, Params> = async (
  context
) => {
  const { slug } = context.params!;
  const locale = context.locale!;

  // 新增下面這段重導向規則
  // Handle redirect logic
  const path = unifyPath('/posts/' + slug);
  const matchedRedirectRule = allRedirects.find((rule) => rule.source === path);
  if (matchedRedirectRule) {
    return {
      redirect: {
        destination: matchedRedirectRule.destination,
        permanent: matchedRedirectRule.permanent,
      },
    };
  }
  // ...
};

// ...

以及新增 src/pages/[...pathToRedirectFrom].tsx,捕捉所有其他路徑:

import { GetStaticPaths, GetStaticProps } from 'next';
import { ParsedUrlQuery } from 'querystring';

import { allRedirects, Redirect } from '@/lib/getAllRedirects';
import { stringifyCatchAllDynamicRoute } from '@/lib/stringifyCatchAllDynamicRoute';
import { unifyPath } from '@/lib/unifyPath';

interface Params extends ParsedUrlQuery {
  pathToRedirectFrom: string | string[];
}

export const getStaticPaths: GetStaticPaths = () => {
  return {
    paths: [],
    fallback: 'blocking',
  };
};

export const getStaticProps: GetStaticProps<any, Params> = (context) => {
  const { pathToRedirectFrom } = context.params!;

  // Handle redirect logic
  const path = unifyPath(stringifyCatchAllDynamicRoute(pathToRedirectFrom));
  const matchedRedirectRule: Redirect | undefined = allRedirects.find(
    (rule) => rule.source === path
  );
  if (matchedRedirectRule) {
    return {
      redirect: {
        destination: matchedRedirectRule.destination,
        permanent: matchedRedirectRule.permanent,
      },
    };
  }

  return {
    notFound: true,
  };
};

const NullComponent = () => null;

export default NullComponent;

設定各文章轉址規則

現在就能使用前面在 Contentlayer Post 新增的 redirectFrom 屬性來指定轉址規則。

例如我把 content/posts/20220904-custom-link-demo.mdx 設定如下:

---
// ...
type: Post
slug: custom-link-demo
redirectFrom:
  - /old-custom-link/
  - /2022/08/01/custom-link/
  - /posts/old-custom-link/
---

Content...

slug 定義了文章現在的路徑,會是 http://localhost:3000/posts/custom-link-demo

redirectFrom 指定了下列 3 條舊路徑,都會導向最新文章路徑:

成果

這樣就完成了!使用 pnpm dev,瀏覽你設定過的舊路徑,就能看你會直接重導向到正確的文章路徑,依然能順利瀏覽文章!

這篇修改的程式碼如下:
https://github.com/Kamigami55/nextjs-tailwind-contentlayer-blog-starter/compare/day28-i18next...day29-old-post-path-redirect

References

小結&下一篇

恭喜你成功在 Next.js Contentlayer blog 實作舊路徑轉址了!

也恭喜你完成這系列文章所有實作了!

下一篇,也就是最後一篇文章,我們會來總結與回顧這 30 篇文章,以及我們一同實作出的炫砲部落格!


上一篇
使用 next-i18next 實作中英文多語系 - Modern Next.js Blog 系列 #28
下一篇
總結與回顧:一個炫砲技術部落格的誕生 - Modern Next.js Blog 系列 #30
系列文
從零開始打造炫砲個人部落格,使用 Next.js、ContentLayer、i18next 等現代技術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言