🔗

Cloudflare PagesでURL短縮サービスをつくる!

2024/03/11に公開

Cloudflare PagesでURL短縮サービスを作ってみましょう!これを作ることであなたは以下を体験することができるしょう。

  • HonoでWebページをつくること
  • Cloudflare KVをアプリケーションの中で使うこと
  • アプリケーションをCloudflare Pagesへデプロイすること

アプリケーションの特徴

今回作ってもらうアプリケーションはこのような特徴があります。

  • Viteを使って開発
  • UI付き
  • JSXを使ってHTMLを書ける
  • メインのコードは100行以下!
  • Zodを使ったバリデーション
  • バリデーションエラーも表示
  • 簡易なCSRF対策

デモ

完成品を使っている様子です。

Demo

完成品

完成済みのコードは以下にあります。

https://github.com/yusukebe/url-shortener

アカウント

今回、アプリケーションを作ってCloudflare PagesへデプロイするにはCloudflareのアカウントが必要です。無料の範囲で遊べるので、もってない人はアカウトを作っておいてください。

プロジェクトのセットアップ

では作っていきます。まず、プロジェクトをセットアップします。

初期プロジェクト

"create-hono"というCLIを使ってプロジェクトを作ります。以下のコマンドを実行します。

npm create hono@latest url-shortener

すると、どのテンプレートにするか選択肢が現れるので"cloudflare-pages"を選びます。その後、依存関係をインストールするか、どのパッケージマネージャーを使うかを聞かれるのでどちらもEnterを押して次に進みます。

これで初期プロジェクトができるので、その中に入っておきましょう。

cd url-shortener

開発サーバーを立ち上げる

開発サーバーを立ち上げてみましょう。といっても簡単です。以下のコマンド実行するだけです。

npm run dev

するとデフォルトでは「http://localhost:5173」で立ち上がるのでそこにアクセスします。ページが見れたでしょう。

KVを作る

このアプリではCloudflare KVというKey-Valueストアを使います。使うためにはKVのプロジェクトを作らなくてはいけないので以下のコマンドを実行します。

npm exec wrangler kv:namespace create KV

すると以下のようなメッセージが表示されます。

🌀 Creating namespace with title "url-shortener-KV"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "KV", id = "xxxxxx" }

このidxxxxxx部分をコピペしておいて、それを以下のフォーマットでwrangler.tomlに書き込みます。

wrangler.toml
[[kv_namespaces]]
binding = "KV"
id = "xxxxxx"

これでKVの設定は終わりです。

依存ライブラリをインストールする

今回のアプリでは入力値の検証をしっかりやります。そのためにZodというライブラリとHonoのミドルウェアを入れます。

npm i zod @hono/zod-validator

publicを消しておく

最後に、スターターテンプレートには自分で書く用のCSSが入っているpublicがあるのですが、今回は使わないので消しておきます。

rm -rf public

コードを書く

ではコードを書いていきましょう。

レイアウトを整える

ページ共通で使う「ガワ」を整えます。そのためにはsrc/renderer.tsxを編集します。

手間はかけたくないので、new.cssというCSSフレームワークを使います。これはClass-lessフレームワークといって、特にclass属性に特別な値を入れずとも、これまでのHTMLそのままでいい感じのスタイルを当ててくれるというものです。

最終的にこのようになりました。

src/renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'

export const renderer = jsxRenderer(({ children }) => {
  return (
    <html>
      <head>
        <link rel="stylesheet" href="https://fonts.xz.style/serve/inter.css" />
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.2/new.min.css"></link>
      </head>
      <body>
        <header>
          <h1>
            <a href="/">URL Shortener</a>
          </h1>
        </header>
        <div>{children}</div>
      </body>
    </html>
  )
})

トップページを作る

最初につくるページはトップページです。GETリクエストが/というパスに来た場合に応答します。ルーティングはこのようになります。

src/index.tsx
app.get('/', (c) => {
  //...
})

ハンドラの中ではc.render()というメソッドの中にJSXを渡せば、レイアウトを適用したHTMLのレスポンスを返します。/createというパスにPOSTリクエストを送って短縮URLをつくるようにしました。

src/index.tsx
app.get('/', (c) => {
  return c.render(
    <div>
      <h2>Create shorten URL!</h2>
      <form action="/create" method="post">
        <input
          type="text"
          name="url"
          autocomplete="off"
          style={{
            width: '80%'
          }}
        />
        &nbsp;
        <button type="submit">Create</button>
      </form>
    </div>
  )
})

すると以下のような見た目になるでしょう。

スクショ

バリデータを作る

トップページからのフォームデータを受け取る際に値の検証をしたいのでそのためのバリデータを作りましょう。

先ほどインストールしたライブラリからオブジェクトをimportします。

src/index.tsx
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

次にスキーマを作ります。「urlという名前でURL形式の文字列を受け取る」ということを以下のように書くことができます。

src/index.tsx
const schema = z.object({
  url: z.string().url()
})

それをzValidatorに登録します。第一引数に渡しているformというのはフォームリクエストをハンドリングしたいから指定しています。

src/index.tsx
const validator = zValidator('form', schema)

/createに来たPOSTリクエストを処理するエンドポイントをつくって、完成したバリデータを使ってみましょう。バリデータはミドルウェアなので、このようにハンドラの前に挟むことができます。そして、c.req.valid()メソッドで値を取得できます。この場合はurlという名前です。

src/index.tsx
app.post('/create', validator, async (c) => {
  const { url } = c.req.valid('form')

  // TODO: Create a short URL

})

検証を通過したらその値がurlに入るでしょう。

KVの型定義をする

フォームからの値が取れたので、次に短縮URLをつくるロジックを書きます。

その前にアプリケーションの中で使うKVの型定義をしておきます。KVNamespaceという型がKVを表します。Honoの場合、Bindingsという名前でCloudflareのBindingsの型をHonoクラスのジェネリクスに渡すとその後、c.env.KVのように型付きでアクセスできるようになります。

src/index.tsx
type Bindings = {
  KV:KVNamespace
}

const app = new Hono<{
  Bindings: Bindings
}>()

キーを生成、保存する

短縮URLのパスに当たるキーを生成する関数をcreateKey()という名前で作りましょう。キーの生成にはKVオブジェクトとURLが必要になります。

src/index.tsx
app.post('/create', validator, async (c) => {
  const { url } = c.req.valid('form')

  const key = await createKey(c.env.KV, url)

  // ...
})

キーを生成するロジックはいくつか考えつきますが、今回はこの作戦でいきます。

  • ランダムな文字列を生成する
  • そのうちの6文字を利用する
  • KVの中にそれをキーとしたオブジェクトがなければURLを値として保存する
  • KVの中にそれをキーとしたオブジェクトがあればもう一度createKey()を実行する
  • 作られたキーを返す

KVはkv.get(key)で値の取得、kv.put(key, value)で値のセットができます。

完成した関数はこのようになりました。

src/index.tsx
const createKey = async (kv: KVNamespace, url: string) => {
  const uuid = crypto.randomUUID()
  const key = uuid.substring(0, 6)
  const result = await kv.get(key)
  if (!result) {
    await kv.put(key, url)
  } else {
    return await createKey(kv, url)
  }
  return key
}

結果を表示する

これでキーの作成ができました。キーの値をパス名にしたURLが短縮URLになります。もしローカルで開発してて、例えば、abcdefだったら

http://localhost:5173/abcdef

となります。そのURLをコピーするのに便利なようにinput要素の中に表示するページをつくりました。autofocusも使っています。

src/index.tsx
app.post('/create', validator, async (c) => {
  const { url } = c.req.valid('form')
  const key = await createKey(c.env.KV, url)

  const shortenUrl = new URL(`/${key}`, c.req.url)

  return c.render(
    <div>
      <h2>Created!</h2>
      <input
        type="text"
        value={shortenUrl.toString()}
        style={{
          width: '80%'
        }}
        autofocus
      />
    </div>
  )
})

すると短縮URLが作成され、いい感じに表示されるようになるでしょう。

スクショ

リダイレクトさせる

短縮URLの生成はできたので、そこにアクセスすると登録されているURLへリダイレクトするようにしましょう。/abcedfというアドレスにマッチさせるには正規表現でルーティングのパスを指定すればいいです。そして、ハンドラの中ではその文字列をキーにKVから値を取得して、ある場合はそれが短縮前のURLなので、そこへリダイレクトさせます。なければアプリのトップページへリダイレクトです。

src/index.tsx
app.get('/:key{[0-9a-z]{6}}', async (c) => {
  const key = c.req.param('key')
  const url = await c.env.KV.get(key)

  if (url === null) {
    return c.redirect('/')
  }

  return c.redirect(url)
})

エラー処理をする

ここまで来るとだいたいできました。いい感じです!

しかし、イケてないのはフォームにURLではない値を入れた場合です。バリデータが機能しているので、しっかりとエラーになるのですが、JSONの文字列が表示されるだけです。

スクショ

エラーページを表示するようにしましょう。そのためにはzValidatorの第3引数にフックを書きます。resultはZodでバリデーションした結果オブジェクトなので、それを使って成功したかどうかを判断しています。

src/index.tsx
const validator = zValidator('form', schema, (result, c) => {
  if (!result.success) {
    return c.render(
      <div>
        <h2>Error!</h2>
        <a href="/">Back to top</a>
      </div>
    )
  }
})

これで、バリデーションエラーが起こった場合はエラーが表示されます。

スクショ

CSRFプロテクターを入れる

これで最後です!今の状態でも十分素晴らしいURL短縮サービスなのですが、異なるサイトのフォームから直接このアプリのフォームにPOSTリクエストがいってしまう場合があります。そこで、HonoのビルドインミドルウェアであるCSRF Protectorを使います。

使い方はとっても簡単。importします。

src/index.tsx
import { csrf } from 'hono/csrf'

使いたいルートのハンドラの前に挟みます。

src/index.tsx
app.post('/create', csrf(), validator, async (c) => {
  const { url } = c.req.valid('form')
  const key = await createKey(c.env.KV, url)
  //...

これで完成です!あなたは100行以内でindex.tsxでUI付き、バリデーション付き、エラー処理付き、CSRF対策付きのURL短縮アプリを作ったのです!

デプロイをする

Cloudflare Pagesへデプロイしてみましょう。以下のコマンドを実行します。

npm run deploy

初めての場合以下のように質問されるので、答えます。

Create a new project
? Enter the name of your new project: › url-shortener
? Enter the production branch name: › main

実行するとデプロイ先のURLが表示されます。おそらく以下のようなものでしょう。

https://random-strings.url-shortener-abc.pages.dev/

作成されてから見ることができるようになるまでしばらく時間がかかるので待ちましょう。場合によっては先頭のホスト名を取り除いたurl-shortener-abc.pages.devにアクセスと見れる場合があります。

ダッシュボードでKVの指定をする

しかし!こままでは「Internal Server Error」が出てると思います。これはKVの設定が本番環境でされてないからです。現状ではwrangler.tomlに設定を書いても、それは反映されず、ダッシュボードでの指定が必要です。作成したPagesプロジェクトの設定画面からKVの項目へいき、KVという名前で先程作成したKVの名前空間を指定しましょう。

スクショ

これで再びデプロイをすると...動いてるはずです!

プロジェクトを消す

使わない人は本番のPagesプロジェクトを消しておきましょう。

まとめ

短縮URLアプリをCloudflare KVとHonoを使って作り、Cloudflare Pagesにデプロイしてみました。メインのsrc/index.tsxが100行ほどですが、「JSONを返すだけ」でもなくしっかりページ付きでバリデーション、エラー処理ができてる立派なアプリになりました。このままだと外部の人が無限に作成できて、無限にKVを叩けるのでその点など考慮すべきは残っているので、そのあたりは宿題にしてください。

以上、いかがでしたでしょうか。いい感じでしょ?Honoを使ったCloudflare Pagesアプリの作成は可能性があるので、試してみてください。また、より大きなアプリを作る場合はファイルベースルーティングができるHonoXの方が便利な場合があるのでそれも使ってみてください。

最後に完成形のレポジトリを再掲します。

https://github.com/yusukebe/url-shortener

Discussion