no7.space
🚚

ブログ記事をリダイレクトできるようにする

Google 検索とかするとまだ旧サイトのときの記事が残ってたりします。
でもリンクをクリックしても、そのパスはもう残ってないため、せっかく訪問してくれても 404 になってしまいます。

なので、救出完了した記事については旧サイト側のパスも用意してリダイレクトできるようにします。

Next.js でリダイレクトする

Next.js でリダイレクトを行うには、 next/navigationredirect を使えばOK。

import { redirect } from 'next/navigation'

redirect(path, type)

path はリダイレクト先のアドレスです
typereplace または 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 は数字とアルファベットで構成されますが、他の文字や数字と混同する可能性がある ILOU は使用しないことになっていますので、これらを覗いた 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')
}

だけとしました。

参考