Next.js + Tailwind CSS でドメインパーキング
はじめに
プライベートでドメインをいくつか持っているのですが、メールアドレスのためだけに持っているドメインや今はあまり使っていないドメインもあり、表示するウェブサイトが特に無いドメインがいくつかあります。何も無いのも寂しいので、ドメイン名だけ表示するようなスカスカの html を作って S3 + CloudFront にデプロイしたりしているのですが、最近 Cloudflare Pages を覚えたのでコイツらも Pages に移そうと考えていました。
まったくスカスカのページなので html ファイルをひとつそのまま移せば良いは良いのですが、せっかく趣味でやっているので Next.js + Tailwind CSS で作り直してみることにします。なかなか使う機会がなく、使ってみたかっただけです。
環境構築
まずは、Next.js と Tailwind CSS で静的サイトを生成するための環境を作っていきます。
Next.js の環境作成
Create Next App でサクッと作成します。TypeScript はもはや基本的人権だという噂なので、オプション付けておきます。
$ 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 を書く際に要素の入れ子を書くのが楽になります。
module.exports = {
plugins: {
'autoprefixer': {},
'tailwindcss/nesting': {},
'tailwindcss': {},
},
}
Tailwind CSS の設定には、Tailwind CSS のクラスを参照する tsx
や css
のパスを設定しておきます。Tailwind CSS v3.0 から、それまでの全部入り CSS から不要なものを Purge するスタイルではなく、最初から必要なものだけ生成するスタイルに変わっている様なので、ちゃんと指定してあげましょう。
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./styles/**/*.css",
],
theme: {
extend: {},
},
plugins: [],
}
最後に global.css
に Tailwind CSS のおまじないを書いておきます。
@tailwind base;
@tailwind components;
@tailwind utilities;
以上で、Tailwind CSS が利用できる様になりました。試しに、雛形のタイトルに underline
を引いてみます。
.title {
@apply underline;
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
開発モードを起動して、http://localhost:3000 を確認してみましょう。
$ npm run dev
ちゃんと、タイトルに underline
が付いていれば、セットアップは完了です。
ドメインパーキングページ
準備ができたので、スカスカのページを作っていきます。
背景にグラデーション
全体にダークな感じのグラデーションをかけてみます。こんな感じで、グラデーションの「向き」と「開始(from)」「中間(via)」「終了(to)」の色を指定するだけです。中間の色は、不要なら指定しなくて大丈夫です。
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;
}
テキストを配置
中央にドメイン名をカード表示し、上下に少しメッセージを書けるようなレイアウトにしてみます。こんな感じ。
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
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;
}
}
トップページ以外へのアクセス
ここまでで、ルート /
に対するアクセスでページを表示できるようになりましたが、うっかり /nothing
のように配下のパスに対してアクセスされた場合は、404 ページが表示されてしまいます。
できれば、同じダークなページを出したいです。
配下のパスへアクセスされても同じパーキングページを表示する
任意のパスパターンをキャッチして同じページを出す仕組みは、Next.js にも備わっていてサンプルもあります。
ですが、この [...slug]
は今回デプロイ先として想定している Cloudflare では動作しませんでした。仕方がないので、404
ページをカスタムして似たようなことを実現してみます。
どうやら pages/404.tsx
ファイルを用意すれば良いようです。とりあえず、pages/index.tsx
と同じ内容をコピーして、動作することを確認しました。
アクセスされた URL の表示
このままだと、ドメイン名の部分が固定文言になっており、ドメインごとに別のプロジェクトを作成するのは面倒そうです。環境変数等から引っ張ってきてビルド時に埋めることも可能かとは思いますが、その設定も面倒なのでクライアントサイドで window.location
から取ってくるのが良さそうです。
まぁ単なる React ですね。普通に useEffect
を使うので良いのですが、ローカルで dev 実行する際に warning が出たので、useIsomorphicLayoutEffect
を使うようにしてみます。結局ブラウザ上でしか動かないので、あまり意味はないかもしれないです。
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.tsx
と pages/404.tsx
にほとんど同じ内容をベタ書きしてしまっているので、少しリファクタリングしておきましょう。
まず、ホスト名等の取得部分は pages/_app.tsx
にまとめて、props
でレイアウトに渡します。
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
にまとめます。ここまでの内容は基本全ページ同一ですが、一応ページごとのファイルを作成すれば、その内容が挿しこまれるようになっています。
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.tsx
と pages/404.tsx
はコレだけになります。
import type { NextPage } from 'next'
const Index: NextPage = () => { return null }
export default Index
import type { NextPage } from 'next'
const Index: NextPage = () => { return null }
export default Index
デプロイ
Cloudflare Pages へのデプロイ
Cloudflare Pages へデプロイする際は、こんな感じのビルドコンフィグで良さそうです(プリセットにもあります)。
Next.js のテンプレートとして利用する
いくつかのサイトに展開するために量産する際には、こんな感じで Create Next App にリポジトリを指定すれば、持ってきてくれる様です。
$ npx create-next-app mydomain --typescript --example https://github.com/a24k/nextailp
PageSpeed Insights の確認
先代のやっぱりスカスカのページは Bootstrap でマークアップしていたのですが、それよりもずっと速いです。めでたしめでたし。
おわりに
ずっと触ってみたかった Next.js を少し触れて楽しかったです。SSG を組み合わせれば、ちょっとしたサイトならすぐ作れそうですね。
なんとなく Vercel にデプロイするのはロックイン感があって Cloudflare Pages を使ってみましたが、Google I/O を見ていたら Firebase が Next.js のデプロイに対応する様です。こちらも楽しみです。
Discussion