🔗

Next.jsで簡易的な短縮URL機能を実装

2024/12/22に公開

URL短縮サービスは、長いURLを短くしてシェアしやすくするための便利なツールです。
Next.js App Routerを使用して、シンプルな短縮URL生成機能を実装してみます。

実装概要

ざっくり以下の流れで短縮URL機能を実装します。

  1. 短縮URLを生成: フォームにURLを入力し、短縮URLを生成してDBに保存。
  2. URLリストの表示: 保存済みの短縮URLを一覧で表示。
  3. リダイレクト処理: 短縮URLにアクセスした際、元のURLにリダイレクト。

実装コード

1. トップページ

src/app/page.tsx

import CreateShortUrl from "./_components/CreateShortUrl";
import { UrlList } from "./_components/UrlList";

export default function Home() {
    return (
        <div className="min-h-screen bg-gray-100 py-10">
            <div className="max-w-4xl mx-auto space-y-10">
                <h1 className="text-center text-2xl font-bold text-gray-800">
                    Short URL Generator
                </h1>
                <CreateShortUrl />
                <UrlList />
            </div>
        </div>
    );
}

2. 短縮URL生成フォーム

src/app/_components/CreateShortUrl.tsx

"use client";

import { useActionState } from "react";
import { createShortUrlAction } from "./action.server";

export default function CreateShortUrl() {
    const [state, formAction, isPending] = useActionState(createShortUrlAction, null);

    return (
        <div className="bg-gray-100 p-6 border-2 border-black rounded-lg max-w-md mx-auto">
            <h2 className="text-xl font-semibold mb-4 text-gray-900">Create Short URL</h2>
            <form action={formAction} className="space-y-4">
                <div>
                    <label className="block text-gray-700 font-medium mb-1">
                        Original URL:
                    </label>
                    <input
                        type="text"
                        name="originalUrl"
                        required
                        className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-black focus:border-black"
                        placeholder="Enter your URL"
                    />
                </div>
                <button
                    type="submit"
                    disabled={isPending}
                    className={`w-full py-2 px-4 font-semibold rounded-md shadow-md transition-colors duration-200 ${
                        isPending
                            ? "bg-gray-300 text-gray-500 cursor-not-allowed"
                            : "bg-black text-white hover:bg-gray-800"
                    }`}
                >
                    {isPending ? "Creating..." : "Create Short URL"}
                </button>
            </form>
            {state && (
                <div className="mt-4">
                    {state.body.shortUrl && (
                        <p className="text-gray-800 font-medium">
                            Short URL:{" "}
                            <a
                                href={state.body.shortUrl}
                                target="_blank"
                                className="underline text-black hover:text-gray-700"
                            >
                                {state.body.shortUrl}
                            </a>
                        </p>
                    )}
                    {state.body.error && (
                        <p className="text-red-600 font-medium">{state.body.error}</p>
                    )}
                </div>
            )}
        </div>
    );
}

3. 短縮URL生成アクション

src/app/_components/action.server.ts

"use server";

import { addShortUrl } from "@/server/lib/db/shorUrl";
import { customAlphabet } from "nanoid";
import { revalidatePath } from "next/cache";

const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz0123456789", 7);

export async function createShortUrlAction(_, formData: FormData) {
    const originalUrl = formData.get("originalUrl") as string;

    if (!originalUrl) {
        return {
            status: 400,
            body: { error: "originalUrl is required" },
        };
    }

    const shortId = nanoid();
    const host = process.env.NEXT_PUBLIC_SHORT_URL_HOST || "http://localhost:3000";
    const shortUrl = `${host}/r/${shortId}`;

    try {
        await addShortUrl(originalUrl, shortId);
    } catch (error) {
        console.error("Error adding short URL:", error);
        return {
            status: 500,
            body: { error: "Failed to save short URL" },
        };
    }

    revalidatePath("/");
    return { status: 200, body: { shortUrl } };
}
  • ランダムな短縮IDを生成し、リダイレクト先URLになる入力されたURLと一緒に保存します

4. URLリストの表示

src/app/_components/UrlList.tsx

import { getUrls } from "@/server/lib/db/shorUrl";

export async function UrlList() {
    const urls = await getUrls();
    const host = process.env.NEXT_PUBLIC_SHORT_URL_HOST || "http://localhost:3000";

    return (
        <div className="bg-gray-100 p-6 border-2 border-black rounded-lg max-w-md mx-auto mt-6">
            <h2 className="text-lg font-semibold mb-4 text-gray-900">URLs</h2>
            <ul className="space-y-2">
                {urls.map((url) => (
                    <li key={url.short_id} className="border p-4">
                        <a href={url.original_url}>{url.original_url}</a>
                        {" → "}
                        <a href={`${host}/r/${url.short_id}`}>{`${host}/r/${url.short_id}`}</a>
                    </li>
                ))}
            </ul>
        </div>
    );
}

5. リダイレクト処理

src/app/r/[short]/page.tsx

import { getOriginalUrl } from "@/server/lib/db/shorUrl";
import { redirect } from "next/navigation";

export default async function RedirectPage({ params }: { params: { short: string } }) {
    const originalUrl = await getOriginalUrl(params.short);

    if (!originalUrl) {
        redirect("/");
    }

    redirect(originalUrl);
}
  • 短縮URLにアクセスすると、まずリダイレクト目的のページにアクセスします。
  • paramsからshortIdを取得し、それに対応するオリジナルのURLをDBから取得してリダイレクトします
  • getOriginalUrl関数内で、DBにクリックカウント用のレコードを記録することで、短縮URLのクリック数のカウントも簡単にできます

Discussion