
ブログ記事をリダイレクトできるようにする
Google 検索とかするとまだ旧サイトのときの記事が残ってたりします。
でもリンクをクリックしても、そのパスはもう残ってないため、せっかく訪問してくれても 404 になってしまいます。
なので、救出完了した記事については旧サイト側のパスも用意してリダイレクトできるようにします。
Next.js でリダイレクトする
Next.js でリダイレクトを行うには、 next/navigation
の redirect
を使えばOK。
import { redirect } from 'next/navigation'
redirect(path, type)
path
はリダイレクト先のアドレスです
type
は replace
または push
からの選択となります。
Note
type のデフォルト値は状況によって異なり、通常は replace
がデフォルト値となりますが、 ServerActions では push
がデフォルトになるみたいです。
やり方を考える
旧パスは、posts/yymmddnn
みたいなパスだったので、ここにルートと対応する markdown ファイルを設置、FrontMatterに redirct
みたいなキーを与えて、これがあってかつ新しいパスに転送先の記事があればリダイレクトする、みたいな手順が良さそうです。
つくってみる
基本的にはブログ記事本文のそれと同じでよさそうですので、大半はそっちから持ち込んで、必要な部分だけ書き換えます。
app/posts/[id]/page.tsx
にファイルを作成します
import fs from 'node:fs/promises'
import path from 'node:path'
import matter from 'gray-matter'
import { notFound, redirect } from 'next/navigation'
import { postTypes } from '@/site.config'
import type { V1PostMetadata } from '@/types/post'
export const dynamic = 'force-static'
type Props = {
params: Promise<{ id: string }>
}
export async function generateStaticParams() {
const dir = path.join(process.cwd(), `${postTypes.posts.contentDir}`)
let files: string[] = []
try {
files = await fs.readdir(dir)
} catch {
return []
}
return files
.filter((f) => f.endsWith('.md'))
.map((f) => ({ id: f.replace(/\.md$/, '') }))
}
export default async function V1PostRedirect({ params }: Props) {
const { id } = await params
const filePath = path.join(
process.cwd(),
postTypes.posts.contentDir,
`${id}.md`
)
let fileContent: string
try {
fileContent = await fs.readFile(filePath, 'utf8')
} catch {
return notFound()
}
const { data } = matter(fileContent)
const fm = data as V1PostMetadata
if (!fm.redirect) {
return notFound()
} else {
// リダイレクト先の投稿IDが、 blog のスラッグパターンにマッチするか確認
if (!postTypes.blog.slugPattern.test(fm.redirect)) {
return notFound()
}
return redirect(`/blog/${fm.redirect}`)
}
}
旧投稿用の FrontMatter の型
V1PostMetadata
markdown の FrontMatter の型ですが、こちらは転送用のキーだけあればよいので、全く別ものとなるため新たに定義しています。
といってもまぁほぼ空みたいなもんですが…
転送先のパスのチェック
if (!postTypes.blog.slugPattern.test(fm.redirect)) {
return notFound()
}
投稿データを作るのは自分だけ、非公開リポジトリでの運用ってことで基本的に自分がミスらない限り平気ではあるんですが、
転送先のURLにそのまま化けるので、パターンチェックをいれてます。
新 URL は ULID を使ってるので、ULID で使用している文字列のみを許可する形で正規表現を作成してます。
const regex = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/
Note
ULID は数字とアルファベットで構成されますが、他の文字や数字と混同する可能性がある I
、L
、O
、U
は使用しないことになっていますので、これらを覗いた 32 文字(アルファベット26文字 - 除外4文字 + 数字10個)で構成される、26 文字のテキストであることを確認しています。
マッチしない場合は 404 画面を表示して終了
転送処理
return redirect(`/blog/${fm.redirect}`)
今回、旧ポスト用の FrontMatter には転送先投稿の ID だけいれてるので、blog
というパスは手で追加してます
また、type は replace で問題ないため、デフォルトのままで良いので省略してます。
親ルートの転送
posts/
へのアクセスは、そのまま blog/
に転送でOKなので、シンプルに
import { redirect } from 'next/navigation'
export default function V1PostListRedirect() {
return redirect('/blog')
}
だけとしました。