⛵️

Next.js + Tailwind CSS でドメインパーキング

2022/05/13に公開

はじめに

プライベートでドメインをいくつか持っているのですが、メールアドレスのためだけに持っているドメインや今はあまり使っていないドメインもあり、表示するウェブサイトが特に無いドメインがいくつかあります。何も無いのも寂しいので、ドメイン名だけ表示するようなスカスカの html を作って S3 + CloudFront にデプロイしたりしているのですが、最近 Cloudflare Pages を覚えたのでコイツらも Pages に移そうと考えていました。

https://pages.cloudflare.com/

まったくスカスカのページなので html ファイルをひとつそのまま移せば良いは良いのですが、せっかく趣味でやっているので Next.js + Tailwind CSS で作り直してみることにします。なかなか使う機会がなく、使ってみたかっただけです。

https://github.com/a24k/nextailp

環境構築

まずは、Next.js と Tailwind CSS で静的サイトを生成するための環境を作っていきます。

Next.js の環境作成

Create Next App でサクッと作成します。TypeScript はもはや基本的人権だという噂なので、オプション付けておきます。

https://nextjs.org/docs/api-reference/create-next-app

$ npx create-next-app nextailp --typescript
$ cd nextailp

静的サイトの生成

Create Next App で生成された雛形の状態では、SSR や Image Optimization など動的な機能が入ってしまっているため、next export が失敗してしまいます。今回は、静的サイトを生成して Cloudflare Pages にアップしたいので、とりあえず動的機能を削除しておきます。

API の削除

$ git rm -r pages/api
rm 'pages/api/hello.ts'

next/image 利用箇所の削除

$ git diff
diff --git a/pages/index.tsx b/pages/index.tsx
index 86b5b3b..1297e39 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -52,19 +52,6 @@ const Home: NextPage = () => {
           </a>
         </div>
       </main>
-
-      <footer className={styles.footer}>
-        <a
-          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Powered by{' '}
-          <span className={styles.logo}>
-            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
-          </span>
-        </a>
-      </footer>
     </div>
   )
 }

$ git rm public/vercel.svg
rm 'public/vercel.svg'

静的サイトの生成

上記を削除した状態で、こんな感じに静的サイトの生成ができる様になります。

$ npx next build
info  - Checking validity of types
info  - Creating an optimized production build
info  - Compiled successfully
info  - Collecting page data
info  - Generating static pages (3/3)
info  - Finalizing page optimization

Page                                       Size     First Load JS
┌ ○ /                                      949 B          75.4 kB
├   └ css/149b18973e5508c7.css             655 B
├   /_app                                  0 B            74.5 kB
└ ○ /404                                   192 B          74.7 kB
+ First Load JS shared by all              74.5 kB
  ├ chunks/framework-00b57966872fc495.js   44.9 kB
  ├ chunks/main-f4ae3437c92c1efc.js        28.3 kB
  ├ chunks/pages/_app-85d7488a393e293e.js  493 B
  ├ chunks/webpack-69bfa6990bb9e155.js     769 B
  └ css/27d177a30947857b.css               194 B

○  (Static)  automatically rendered as static HTML (uses no initial props)


$ npx next export
info  - using build directory: /path/to/nextailp/.next
info  - Copying "static build" directory
info  - No "exportPathMap" found in "/path/to/nextailp/next.config.js". Generating map from "./pages"
info  - Launching 9 workers
info  - Copying "public" directory
info  - Exporting (2/2)
Export successful. Files written to /path/to/nextailp/out

Tailwind CSS の導入

続いて Tailwind CSS を導入していきます。

$ npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
$ npx tailwindcss init -p

PostCSS の設定に、tailwindcss/nesting を追加します。これを入れておくと、CSS を書く際に要素の入れ子を書くのが楽になります。

postcss.config.js
module.exports = {
  plugins: {
    'autoprefixer': {},
    'tailwindcss/nesting': {},
    'tailwindcss': {},
  },
}

Tailwind CSS の設定には、Tailwind CSS のクラスを参照する tsxcss のパスを設定しておきます。Tailwind CSS v3.0 から、それまでの全部入り CSS から不要なものを Purge するスタイルではなく、最初から必要なものだけ生成するスタイルに変わっている様なので、ちゃんと指定してあげましょう。

taailwind.config.js
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./styles/**/*.css",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

最後に global.css に Tailwind CSS のおまじないを書いておきます。

styles/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;

以上で、Tailwind CSS が利用できる様になりました。試しに、雛形のタイトルに underline を引いてみます。

styles/Home.modules.css
.title {
  @apply underline;
  margin: 0;
  line-height: 1.15;
  font-size: 4rem;
}

開発モードを起動して、http://localhost:3000 を確認してみましょう。

$ npm run dev

screen0001

ちゃんと、タイトルに underline が付いていれば、セットアップは完了です。

ドメインパーキングページ

準備ができたので、スカスカのページを作っていきます。

背景にグラデーション

全体にダークな感じのグラデーションをかけてみます。こんな感じで、グラデーションの「向き」と「開始(from)」「中間(via)」「終了(to)」の色を指定するだけです。中間の色は、不要なら指定しなくて大丈夫です。

styles/global.css
body {
  @apply p-0 m-0;
  @apply bg-zinc-900 bg-gradient-to-tr from-zinc-900 via-zinc-800 to-zinc-900;
  @apply font-mono;
}

テキストを配置

中央にドメイン名をカード表示し、上下に少しメッセージを書けるようなレイアウトにしてみます。こんな感じ。

pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import Link from 'next/link'

const Index: NextPage = () => {
  return (
    <>
      <Head>
        <title>nextailp</title>
        <meta name="description" content="a domain parking example based on Next.js & Tailwind CSS." />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>

        <p>+ NOT IN SERVICE +</p>

        <div className="card">
          <h1><Link href="https://nextailp.pages.dev"><a>nextailp.pages.dev</a></Link></h1>
        </div>

        <p>powered by <Link href="https://github.com/a24k/nextailp"><a>nextailp</a></Link> - a domain parking example based on <Link href="https://nextjs.org/"><a>Next.js</a></Link> & <Link href="https://tailwindcss.com/"><a>Tailwind CSS</a></Link>.</p>

      </main>
    </>
  )
}

export default Index
styles/global.css
main {
  @apply h-screen;
  @apply flex flex-col justify-center items-center;

  .card {
    @apply p-16 max-w-screen-sm;
    @apply rounded-xl;
    @apply bg-white shadow-lg shadow-white/20;

    h1, h2 {
      @apply my-2;
      @apply text-zinc-800 text-center;
    }

    h1 {
      @apply text-4xl break-all;
    }

    h2 {
      @apply text-2xl truncate;
    }
  }

  p {
    @apply p-4;
    @apply text-base text-zinc-500;
  }
}

screen0002

トップページ以外へのアクセス

ここまでで、ルート / に対するアクセスでページを表示できるようになりましたが、うっかり /nothing のように配下のパスに対してアクセスされた場合は、404 ページが表示されてしまいます。

screen0003

できれば、同じダークなページを出したいです。

配下のパスへアクセスされても同じパーキングページを表示する

任意のパスパターンをキャッチして同じページを出す仕組みは、Next.js にも備わっていてサンプルもあります。

https://github.com/vercel/next.js/tree/canary/examples/catch-all-routes

ですが、この [...slug] は今回デプロイ先として想定している Cloudflare では動作しませんでした。仕方がないので、404 ページをカスタムして似たようなことを実現してみます。

https://nextjs.org/docs/advanced-features/custom-error-page

どうやら pages/404.tsx ファイルを用意すれば良いようです。とりあえず、pages/index.tsx と同じ内容をコピーして、動作することを確認しました。

screen0004

アクセスされた URL の表示

このままだと、ドメイン名の部分が固定文言になっており、ドメインごとに別のプロジェクトを作成するのは面倒そうです。環境変数等から引っ張ってきてビルド時に埋めることも可能かとは思いますが、その設定も面倒なのでクライアントサイドで window.location から取ってくるのが良さそうです。

https://nextjs.org/docs/basic-features/data-fetching/client-side

まぁ単なる React ですね。普通に useEffect を使うので良いのですが、ローカルで dev 実行する際に warning が出たので、useIsomorphicLayoutEffect を使うようにしてみます。結局ブラウザ上でしか動かないので、あまり意味はないかもしれないです。

pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import Link from 'next/link'
import { useState } from 'react'
import { useIsomorphicLayoutEffect } from 'usehooks-ts'

const Index: NextPage = () => {
  const [host, setHost] = useState<string>('nextailp.pages.dev');
  const [path, setPath] = useState<string>('/');

  useIsomorphicLayoutEffect(() => {
    setHost(window.location.host);
    setPath(window.location.pathname);
  });

  return (
    <>
      <Head>
        <title>nextailp</title>
        <meta name="description" content="a domain parking example based on Next.js & Tailwind CSS." />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>

        <p>+ NOT IN SERVICE +</p>

        <div className="card">
          <h1><Link href="/"><a>{host}</a></Link></h1>
          <h2>{path}</h2>
        </div>

        <p>powered by <Link href="https://github.com/a24k/nextailp"><a>nextailp</a></Link> - a domain parking example based on <Link href="https://nextjs.org/"><a>Next.js</a></Link> & <Link href="https://tailwindcss.com/"><a>Tailwind CSS</a></Link>.</p>

      </main>
    </>
  )
}

export default Index

レイアウトを集約

だいたい完成なのですが、pages/index.tsxpages/404.tsx にほとんど同じ内容をベタ書きしてしまっているので、少しリファクタリングしておきましょう。

まず、ホスト名等の取得部分は pages/_app.tsx にまとめて、props でレイアウトに渡します。

pages/_app.tsx
import type { AppProps } from 'next/app'

import '../styles/globals.css'
import Layout from '../components/layout'

import { useState } from 'react'
import { useIsomorphicLayoutEffect } from 'usehooks-ts'

function NextailpApp({ Component, pageProps }: AppProps) {
  const [host, setHost] = useState<string>('nextailp.pages.dev');
  const [path, setPath] = useState<string>('/');

  useIsomorphicLayoutEffect(() => {
    setHost(window.location.host);
    setPath(window.location.pathname);
  });

  const layoutProps = { host, path };

  return (
    <Layout {...layoutProps}>
      <Component {...pageProps} />
    </Layout>
  )
}

export default NextailpApp

レイアウトは、components/layout.tsx にまとめます。ここまでの内容は基本全ページ同一ですが、一応ページごとのファイルを作成すれば、その内容が挿しこまれるようになっています。

components/layout.tsx
import type { ReactElement } from 'react'

import Head from 'next/head'
import Link from 'next/link'

type LayoutProps = Required<{
  readonly host: string;
  readonly path: string;
  readonly children: ReactElement
}>

const Layout = ({ host, path, children }: LayoutProps) => {
  return (
    <>
      <Head>
        <title>{host}</title>
        <meta name="description" content="a domain parking example based on Next.js & Tailwind CSS." />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <p>+ NOT IN SERVICE +</p>

        <div className="card">
          <h1><Link href="/"><a>{host}</a></Link></h1>
          { (path !== '/') && <h2>{path}</h2> }
          {children}
        </div>

        <p>powered by <Link href="https://github.com/a24k/nextailp"><a>nextailp</a></Link> - a domain parking example based on <Link href="https://nextjs.org/"><a>Next.js</a></Link> & <Link href="https://tailwindcss.com/"><a>Tailwind CSS</a></Link>.</p>
      </main>
    </>
  )
}

export default Layout

これにより、pages/index.tsxpages/404.tsx はコレだけになります。

pages/index.tsx
import type { NextPage } from 'next'

const Index: NextPage = () => { return null }

export default Index

screen0005

pages/404.tsx
import type { NextPage } from 'next'

const Index: NextPage = () => { return null }

export default Index

screen0006

デプロイ

Cloudflare Pages へのデプロイ

Cloudflare Pages へデプロイする際は、こんな感じのビルドコンフィグで良さそうです(プリセットにもあります)。

screen0007

Next.js のテンプレートとして利用する

いくつかのサイトに展開するために量産する際には、こんな感じで Create Next App にリポジトリを指定すれば、持ってきてくれる様です。

$ npx create-next-app mydomain --typescript --example https://github.com/a24k/nextailp

PageSpeed Insights の確認

先代のやっぱりスカスカのページは Bootstrap でマークアップしていたのですが、それよりもずっと速いです。めでたしめでたし。

screen0008

おわりに

ずっと触ってみたかった Next.js を少し触れて楽しかったです。SSG を組み合わせれば、ちょっとしたサイトならすぐ作れそうですね。

なんとなく Vercel にデプロイするのはロックイン感があって Cloudflare Pages を使ってみましたが、Google I/O を見ていたら Firebase が Next.js のデプロイに対応する様です。こちらも楽しみです。

https://github.com/FirebaseExtended/firebase-framework-tools/

Discussion