TailwindブログをNext.jsで構築する

Adam Wathan

私たちチームが信じていることの一つは、私たちが作るすべてのものはブログ記事で発表されるべきということです。すべてのプロジェクトについて短い告知記事を書くことを自分たちに義務付けることで、組み込みの品質チェックとして機能し、プロジェクトが「完了」したと世界に自信を持って言えるまで決して言わないようにしています。

問題は、今日まで、実際にそれらの投稿を公開する場所がなかったことです!

プラットフォームの選択

私たちは開発者のチームなので、当然ながら既製品を使うことに自分たちを納得させることはできず、Next.jsを使ってシンプルでカスタムなものを構築することにしました。

Next.jsには多くの良い点がありますが、私たちがそれを使うことに決めた主な理由は、投稿を作成するために使いたいフォーマットであるMDXを非常に良くサポートしているからです。

# My first MDX post
MDX is a really cool authoring format because it lets
you embed React components right in your markdown:
<MyComponent myProp={5} />
How cool is that?

MDXは、通常のMarkdownとは異なり、ライブReactコンポーネントをコンテンツに直接埋め込むことができるため、非常に興味深いです。これは、文章でアイデアを伝える方法に多くの機会を開くため、エキサイティングです。画像、ビデオ、またはコードブロックのみに頼る代わりに、インタラクティブなデモを構築し、Markdownで作成する人間工学を損なうことなく、コンテンツの2つの段落の間に直接貼り付けることができます。

私たちは今年後半にTailwind CSSドキュメントサイトの再設計と再構築を計画しており、インタラクティブなコンポーネントを埋め込むことができることは、フレームワークの仕組みを教える能力に大きな違いをもたらすため、小さなブログサイトをテストプロジェクトとして使用することは非常に理にかなっていました。

コンテンツの整理

私たちは、投稿をpagesディレクトリに直接配置されたシンプルなMDXドキュメントとして書き始めることから始めました。しかし、最終的には、ほとんどすべての投稿に、少なくともOpen Graphイメージなどの関連アセットも必要になることに気付きました。

それらを別のフォルダに保存しなければならないのは少しだらしなく感じたので、代わりに、すべての投稿にpagesディレクトリに独自のフォルダを与え、投稿コンテンツをindex.mdxファイルに入れることにしました。

public/
src/
├── components/
├── css/
├── img/
└── pages/
├── building-the-tailwindcss-blog/
│ ├── index.mdx
│ └── card.jpeg
├── introducing-linting-for-tailwindcss-intellisense/
│ ├── index.mdx
│ ├── css.png
│ ├── html.png
│ └── card.jpeg
├── _app.js
├── _document.js
└── index.js
next.config.js
package.json
postcss.config.js
README.md
tailwind.config.js

これにより、その投稿のすべてのアセットを同じフォルダに共配置し、webpackのfile-loaderを利用して、それらのアセットを投稿に直接インポートすることができます。

メタデータ

各投稿に関するメタデータを、各MDXファイルの先頭でエクスポートするmetaオブジェクトに保存します

import { bradlc } from "@/app/blog/authors";
import openGraphImage from "./card.jpeg";
export const meta = {
title: "Introducing linting for Tailwind CSS IntelliSense",
description: `Today we’re releasing a new version of the Tailwind CSS IntelliSense extension for Visual Studio Code that adds Tailwind-specific linting to both your CSS and your markup.`,
date: "2020-06-23T18:52:03Z",
authors: [bradlc],
image: openGraphImage,
discussion: "https://github.com/tailwindcss/tailwindcss/discussions/1956",
};
// Post content goes here

ここでは、投稿タイトル(投稿ページの実際のh1とページタイトルに使用)、説明(Open Graphプレビュー用)、公開日、著者、Open Graphイメージ、および投稿のGitHub Discussionsスレッドへのリンクを定義します。

すべての著者データを、各チームメンバーの名前、Twitterハンドル、およびアバターのみを含む別のファイルに保存します。

import adamwathanAvatar from "./img/adamwathan.jpg";
import bradlcAvatar from "./img/bradlc.jpg";
import steveschogerAvatar from "./img/steveschoger.jpg";
export const adamwathan = {
name: "Adam Wathan",
twitter: "@adamwathan",
avatar: adamwathanAvatar,
};
export const bradlc = {
name: "Brad Cornes",
twitter: "@bradlc",
avatar: bradlcAvatar,
};
export const steveschoger = {
name: "Steve Schoger",
twitter: "@steveschoger",
avatar: steveschogerAvatar,
};

著者オブジェクトを何らかの識別子を介して接続するのではなく、実際に投稿にインポートすることの良い点は、必要に応じて著者をインラインで簡単に追加できることです

export const meta = {
title: "An example of a guest post by someone not on the team",
authors: [
{
name: "Simon Vrachliotis",
twitter: "@simonswiss",
avatar: "https://pbs.twimg.com/profile_images/1160929863/n510426211_274341_6220_400x400.jpg",
},
],
// ...
};

これにより、著者情報に中央の信頼できる情報源を与えることで、著者情報を簡単に同期させることができますが、柔軟性を損なうことはありません。

投稿プレビューの表示

ホームページに各投稿のプレビューを表示したいと考えていましたが、これは驚くほど難しい問題であることがわかりました。

基本的にやりたかったことは、Next.jsのgetStaticProps機能を使って、ビルド時にすべての投稿のリストを取得し、必要な情報を抽出し、それをレンダリングする実際のページコンポーネントに渡すことでした。

課題は、実際にすべてのページをインポートせずにこれを行いたいということです。なぜなら、それはホームページのバンドルにサイト全体のすべてのブログ投稿が含まれることを意味し、必要以上に大きなバンドルにつながるからです。今は数件の投稿しかないときは大したことではないかもしれませんが、数十件または数百件の投稿になると、多くのバイトが無駄になります。

いくつかの異なるアプローチを試しましたが、最終的に落ち着いたのは、webpackのresourceQuery機能と、各ブログ投稿を2つの形式でロードできるようにするいくつかのカスタムローダーを組み合わせることでした

  1. 投稿ページで使用される投稿全体。
  2. ホームページに必要な最小限のデータをロードする投稿プレビュー。

設定方法では、個々の投稿のインポートの最後に?previewクエリを追加すると、投稿全体の内容ではなく、メタデータとプレビュー抜粋のみを含む、はるかに小さいバージョンの投稿が返されます。

カスタムローダーがどのようなものかのスニペットを次に示します

{
resourceQuery: /preview/,
use: [
...mdx,
createLoader(function (src) {
if (src.includes('<!--​more​-->')) {
const [preview] = src.split('<!--​more​-->')
return this.callback(null, preview)
}
const [preview] = src.split('<!--​/excerpt​-->')
return this.callback(null, preview.replace('<!--​excerpt​-->', ''))
}),
],
},

これにより、各投稿の抜粋を、導入段落の後に<!--​more-->を貼り付けるか、抜粋を<!--​excerpt--><!--​/excerpt-->タグのペアでラップすることによって定義できます。これにより、投稿コンテンツから完全に独立した抜粋を作成できます。

export const meta = {
// ...
}
This is the beginning of the post, and what we'd like to
show on the homepage.
<!--​more-->
Anything after that is not included in the bundle unless
you are actually viewing that post.

この問題をエレガントな方法で解決することは非常に困難でしたが、最終的には、プレビューと実際の投稿コンテンツのために別のファイルを使用する代わりに、すべてを1つのファイルに保持できるソリューションを思いつくことができてクールでした。

次/前の投稿リンクの生成

このシンプルなサイトを構築する際に私たちが抱えていた最後の課題は、個々の投稿を表示するときはいつでも、次および前の投稿へのリンクを含めることができるようにすることでした。

その核となる部分は、すべての投稿(理想的にはビルド時)をロードアップし、リスト内の現在の投稿を見つけ、次に前後の投稿を取得して、それらをページコンポーネントにpropsとして渡せるようにする必要がありました。

これは予想以上に困難であることが判明しました。なぜなら、MDXは現在、通常使用する方法でgetStaticPropsをサポートしていないことが判明したからです。MDXファイルから直接エクスポートすることはできません。代わりに、コードを別のファイルに保存し、そこから再エクスポートする必要があります。

ホームページで投稿プレビューをインポートするだけでこの追加コードをロードしたくありませんでした。また、このコードをすべての投稿で繰り返す必要もありませんでした。そのため、別のカスタムローダーを使用して、このエクスポートを各投稿の先頭にプリペンドすることにしました

{
use: [
...mdx,
createLoader(function (src) {
const content = [
'import Post from "@/components/Post"',
'export { getStaticProps } from "@/getStaticProps"',
src,
'export default (props) => <Post meta={meta} {...props} />',
].join('\n')
if (content.includes('<!--​more-->')) {
return this.callback(null, content.split('<!--​more-->').join('\n'))
}
return this.callback(null, content.replace(/<!--​excerpt-->.*<!--\/excerpt-->/s, ''))
}),
],
}

また、これらの静的propsを実際にPostコンポーネントに渡すためにこのカスタムローダーを使用する必要があったため、上記に表示される追加のエクスポートも追加しました。

ただし、これが唯一の問題ではありませんでした。getStaticPropsは、レンダリングされている現在のページに関する情報を何も提供しないことが判明したため、次および前の投稿を決定しようとするときに、どの投稿を見ているのかを知る方法がありませんでした。これは解決可能だと思いますが、時間の制約により、クライアント側でより多くの作業を行い、ビルド時の作業を減らすことにしました。そのため、必要なリンクを把握しようとするときに、現在のルートが実際にどうなっているかを確認できます。

getStaticPropsですべての投稿をロードアップし、投稿のURLと投稿タイトルのみを含む非常に軽量なオブジェクトにマップします

import getAllPostPreviews from "@/getAllPostPreviews";
export async function getStaticProps() {
return {
props: {
posts: getAllPostPreviews().map((post) => ({
title: post.module.meta.title,
link: post.link.substr(1),
})),
},
};
}

次に、実際のPostレイアウトコンポーネントで、現在のルートを使用して次および前の投稿を決定します

export default function Post({ meta, children, posts }) {
const router = useRouter();
const postIndex = posts.findIndex((post) => post.link === router.pathname);
const previous = posts[postIndex + 1];
const next = posts[postIndex - 1];
// ...
}

これは今のところ十分に機能しますが、長期的には、getStaticPropsで投稿全体ではなく、次および前の投稿のみをロードできるよりシンプルなソリューションを見つけたいと思います。

Hashicorpによる興味深いライブラリNext MDX Remoteがあり、MDXファイルをデータソースのように扱うことを可能にするように設計されています。これは、おそらく将来検討するでしょう。これにより、動的なスラッグベースのルーティングに切り替えることができるはずです。これにより、getStaticPropsで現在のパス名にアクセスできるようになり、さらに多くの機能が得られます。

まとめ

全体として、Next.jsでこの小さなサイトを構築することは、楽しい学習経験でした。これらのツールの多くでは、一見単純なことが最終的にどれほど複雑になるかにいつも驚かされますが、Next.jsの将来に非常に強気であり、今後数か月でtailwindcss.comの次のイテレーションをそれを使って構築することを楽しみにしています。

このブログのコードベースをチェックしたり、上記のいずれかを簡素化するためのプルリクエストを送信したりすることに興味がある場合は、GitHubでリポジトリを確認してください

この投稿について話したいですか? GitHubで議論する →

すべての最新情報を直接あなたの受信箱に。
ニュースレターにサインアップしてください。

著作権 © 2025 Tailwind Labs Inc.·商標ポリシー