Next 9.4 と Netlify で Markdown ブログを作成
このガイドの最終更新は 2020 年 6 月 3 日(水)で、Next 9.4 以降に対応しています。
Next についてお話します。え?「次」というからには、その前は?アハハ!冗談はさておき、Next.js とは、React アプリケーションを構築するためのフレームワークです。Netlify と組み合わせて使用すると、すばらしい Jamstack アプリケーションを構築できることは間違いありません。Next は、ファイルベースのルーティングという特性と、開始の容易さと、柔軟性により、かなりの普及に成功しています。実際、非常に柔軟性にすぐれているため、プロジェクトやサイトへの実装においてルールがやかましくありません。何はともあれ、最新の Next(ブログ執筆時点で 9.4.4)を使用して Markdown ブログを作成する方法、および Netlify にデプロイする方法について説明します。
無駄話をすべて飛ばしてデプロイに直接進みたいという方は、このプロジェクトのリポジトリをチェックしてください。[Deploy to Netlify](Netlify へのデプロイ)ボタンは、あなたに代わって大変な作業をすべて行ってくれる、すばらしいボタンです。好みに合わせて簡単にカスタマイズできます。
このチュートリアルは、React、JavaScript、CSS の知識があることを前提としています。これらの分野でヘルプが必要な場合は、Netlify コミュニティで遠慮なく質問してください!
In this post
Next.js プロジェクトのセットアップ
新しい Next アプリを起動するには、端末を開いて以下を実行します。
npm init next-app
プロンプトが表示され、プロジェクト名と開始テンプレートの入力を求められます。続行して、このアプリについて[Default starter app](デフォルトのスターターアプリ)を選択します。
次に、プロジェクト内を移動し、最上位に siteconfig.json
を追加します。タイトルと説明を入力します。私は次のように入力しました。
{
"title": "Demo Blog",
"description": "This is a simple blog built with Next, easily deployable to Netlify!"
}
幸いにも、Next はビギナーがうんざりするような要素はありません。最初に存在するディレクトリは pages/
および public/
ディレクトリのみです。次に、フォルダー構造を次のように設定します。
components/
pages/
post/
posts/
public/
static/
これにより、エディターにすべてを同時に表示できます。
コーディングする
すべての設定が完了しました。それでは、index.js
内のすべてのものを次のように置き換えます。
const Index = ({ title, description, ...props }) => {
return <div>Hello, world!</div>
}
export default Index
export async function getStaticProps() {
const configData = await import(`../siteconfig.json`)
return {
props: {
title: configData.default.title,
description: configData.default.description,
},
}
}
この時点で、必要に応じて、public
フォルダーに独自のカスタムファビコンを配置し、コードに付属していたものと置き換えることができます。私は 1 つにまとめた小さな Netlify ロゴを用意しているので、それを使用します。この方法がわからない場合は、私が個人的に使用している Favicon & App Icon Generator サイトをご覧ください。
端末で、npm run dev
を実行します。出来上がりです!Next アプリが起動し、実行されています!
React をご存じなら、この index.js
ファイルは驚くほどのものではないでしょう。しかし、その getStaticProps
関数については説明したいと思います。この関数は Next 9.3 でリリースされました。この関数を使用すると、データをフェッチして、ページコンポーネントに props として返すことができます。getStaticProps
を使用すると、ローカルデータをフェッチ(ここに表示されているように、siteconfig.json
ファイルからフェッチ)、または外部 API およびライブラリをフェッチできます。この関数は、pages
ディレクトリ内のページコンポーネントに対してのみ、機能します。ビルド時にページがレンダリングされます。そのデータをページの子コンポーネントに渡すことができます。これから、それらの子コンポーネントの一部を実装して、動作を確認しましょう。
コンポーネント!
ここでは、実際のルーティングとコンポーネントをいくつか紹介します。コンポーネントから始めます。components
フォルダー内に Header.js
、Layout.js
、および PostList.js
の 3 つの JS ファイルを作成しましょう。
Header.js
ファイルには、サイトヘッダーとナビゲーションが格納されます。PostList.js
ファイルには、ご想像のとおり、ブログ記事がリストされます。Layout.js
ファイルはジューシーなファイルです。Header を取得し、HTML の<head>
タグを追加し、サイトに保持されているすべてのコンテンツを格納し、そこにフッターを挿入します。
まず Layout.js
を実装し、ホームページに取り込みましょう。Layout.js
を開き、ここで確認します。
import Head from 'next/head'
import Header from './Header'
export default function Layout({ children, pageTitle, ...props }) {
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{pageTitle}</title>
</Head>
<section className="layout">
<div className="content">{children}</div>
</section>
<footer>Built by me!</footer>
</>
)
}
ここで Head
コンポーネントは next/head
からインポートされていることに注目してください。Next では、レンダリングされる HTML ページの <head>
にタグを含める場合は常に、このエレメントを使用します!この例では、1 つの <meta>
タグと <title>
のみを含めましたが、好きなだけ追加できます。
ファイルの残りの部分は、レイアウト用の基本的なブロックをレンダリングするのみです。それでは、index.js
を開き、「hello world」<div>
を次のように置き換えましょう。
const Index = ({ title, description, ...props }) => {
+ return (
+ <Layout pageTitle={title}>
+ <h1 className="title">Welcome to my blog!</h1>
+ <p className="description">
+ {description}
+ </p>
+ <main>
+ Posts go here!
+ </main>
+ </Layout>)
}
また、index.js の先頭に Layout
コンポーネントをインポートしてください。
import Layout from '../components/Layout'
それでは、ブラウザでサイトをチェックしましょう!
すばらしいですね!パラグラフ内の説明とブラウザタブ上のタイトルが getStaticProps
からどのように返されているかに注目してください。使用しているのは実際のデータです!
ここで、Header
コンポーネントを追加しましょう。Header.js
を開き、以下を追加します。
import Link from 'next/link'
export default function Header() {
return (
<>
<header className="header">
<nav className="nav">
<Link href="/">
<a>My Blog</a>
</Link>
<Link href="/about">
<a>About</a>
</Link>
</nav>
</header>
</>
)
}
では、少し分析しましょう。Link
タグが使用されていることに注目してください。クライアント側のルート間の遷移は、このタグによって有効になります。ここには非常に優れた API が組み込まれています(こちらでチェックしてください)。ただし、この例で知っておくべきことが 2 つあります。href
は唯一の必須 prop であること(そして pages
ディレクトリ内からのパスを指していること)、およびサイトの SEO に対応してリンクを適切に構築するために <a>
コンポーネントをここに配置する必要があることです。
OK です。このコンポーネントを作成したので、これを Layout
コンポーネントに取り込みましょう。<section>
タグの先頭に <Header />
タグを挿入します。Header
コンポーネントはインポート済みなので、ロードさせるだけで大丈夫です!
export default function Layout({ children, pageTitle, ...props }) {
return (
<>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{pageTitle}</title>
</Head>
<section className="layout">
+ <Header />
<div className="content">{children}</div>
</section>
<footer>Built by me!</footer>
</>
)
}
ブラウザでチェックしてみましょう。
やってしまいました。確かに、ナビゲーションはあります!ただし、お気づきかもしれませんが、About リンクをクリックすると 404 が表示されます。修正する必要がありますね。
ページコンポーネントの追加
では、ナビゲーションによる移動を組み込みたいので、(対象の記事がある場合は特に)移動を可能にする必要があります。
pages
フォルダーに about.js
ファイルを作成し、次のように入力します。
import Layout from '../components/Layout'
const About = ({ title, description, ...props }) => {
return (
<>
<Layout pageTitle={`${title} | About`} description={description}>
<h1 className="title">Welcome to my blog!</h1>
<p className="description">
{description}
</p>
<p>
I am a very exciting person. I know this because I'm following a very exciting tutorial, and a not-exciting person wouldn't do that.
</p>
</Layout>
</>
)
}
export default About
export async function getStaticProps() {
const configData = await import(`../siteconfig.json`)
return {
props: {
title: configData.default.title,
description: configData.default.description,
},
}
}
これは index.js
ファイルとよく似ています。今のところ、いいですね。このファイルには何でも追加できます。必要に応じて、ここに他のファイルを追加することもできます。それでは、ブラウザに戻り、ナビゲーションの About リンクをクリックしてみましょう。
ページがあります!ワクワクしますか?ワクワクしますね。ダイナミックルートについてのお話しで、さらにワクワクしましょう。
ダイナミックルート
さて、ページ間で移動したいとき、そのためのファイルを作成するだけでよいこと、およびルートは存在していることはもう知っていると思います。pages
ディレクトリに ilovenetlify.js
ファイルを作成した場合は、localhost:3000/ilovenetlify
に移動できます。これは普通に機能するでしょう。では、ダイナミックタイトルを持つブログ記事があり、それらのタイトルの後ろにルートの名前を指定する場合はどうでしょうか?心配ありません。Next が解決してくれます。
それでは、pages/post
に [postname].js
というファイルを作成します。おや、ファイル名に角括弧が付いています!角括弧により、Next はダイナミックルートの存在を認識します。ファイルパス pages/post/[postname].js
は最終的にブラウザで localhost:3000/post/:postname
となります。そうです。ここにはネストされたルートとダイナミックルートの両方があります。どなたか、Web 開発警察を呼んでください。心当たりがあります!
Markdown の処理
[postname].js
を追加する前に、他のものもいくつかプロジェクトにインストールする必要があります。
npm install react-markdown gray-matter raw-loader
- React Markdown は、Markdown のファイルを解析およびレンダリングするライブラリです。
- Gray Matter は、ブログ記事(必要なメタデータを含む Markdown 記事の部分)の YAML front matter を解析します。
- Raw Loader は、webpack で Markdown のファイルをインポートできるローダーです。
次に、最上位で next.config.js
ファイルを作成し、次のように入力します。
module.exports = {
target: 'serverless',
webpack: function (config) {
config.module.rules.push({
test: /\.md$/,
use: 'raw-loader',
})
return config
},
}
注:このファイルを追加した後で、おそらく dev server の再起動が必要になるでしょう。
ついて来ていますか?これらのライブラリをセットアップしたので、すべてを使いこなすために、[postname].js
を実際にセットアップします。ファイルに以下を挿入します。
import Link from 'next/link'
import matter from 'gray-matter'
import ReactMarkdown from 'react-markdown'
import Layout from '../../components/Layout'
export default function BlogPost({ siteTitle, frontmatter, markdownBody }) {
if (!frontmatter) return <></>
return (
<Layout pageTitle={`${siteTitle} | ${frontmatter.title}`}>
<Link href="/">
<a>Back to post list</a>
</Link>
<article>
<h1>{frontmatter.title}</h1>
<p>By {frontmatter.author}</p>
<div>
<ReactMarkdown source={markdownBody} />
</div>
</article>
</Layout>
)
}
export async function getStaticProps({ ...ctx }) {
const { postname } = ctx.params
const content = await import(`../../posts/${postname}.md`)
const config = await import(`../../siteconfig.json`)
const data = matter(content.default)
return {
props: {
siteTitle: config.title,
frontmatter: data.data,
markdownBody: data.content,
},
}
}
export async function getStaticPaths() {
const blogSlugs = ((context) => {
const keys = context.keys()
const data = keys.map((key, index) => {
let slug = key.replace(/^.*[\\\/]/, '').slice(0, -3)
return slug
})
return data
})(require.context('../../posts', true, /\.md$/))
const paths = blogSlugs.map((slug) => `/post/${slug}`)
return {
paths,
fallback: false,
}
}
はあ…長いので分割しましょう。
getStaticProps
で、posts
ディレクトリにある Markdown のファイルを取得します。また、ブラウザタブのサイトのタイトルを取得するために、siteconfig.json
を再度取得します。Grey Matter を使用してブログ記事の front matter を解析します。次に、これをすべてコンポーネントの props として返します。getStaticPaths
もあります。これも Next 9.3 の新機能でした。この関数は、ビルド時に HTML にレンダリングする必要があるパスのリストを定義します。そのため、ここで必要となるのが、posts
ディレクトリ内の Markdown のファイルをすべて取得し、ファイル名を解析し、各ファイル名に基づいてスラグのリストを定義し、それらを返すことです。また、fallback
: false を返し、適切でないものがあれば 404 ページが表示されるようにします。BlogPost
コンポーネント自体では、getStaticProps
に与えられた値を使用してLayout
コンポーネントにブログ記事コンテンツを入力します。
OK です!多くの作業を行いましたが、結果の多くはまだ見えていません。これを変えましょう。posts/
ディレクトリの最上位に mypost.md
ファイルを作成し、次のように入力します。
---
title: 'Hello, world!'
author: 'Cassidy'
---
Humblebrag sartorial man braid ad vice, wolf ramps in cronut proident cold-pressed occupy organic normcore. Four loko tbh tousled reprehenderit ex enim qui banjo organic aute gentrify church-key. Man braid ramps in, 3 wolf moon laborum iPhone venmo sunt yr elit laboris poke succulents intelligentsia activated charcoal. Gentrify messenger bag hot chicken brooklyn. Seitan four loko art party, ut 8-bit live-edge heirloom. Cornhole post-ironic glossier officia, man braid raclette est organic knausgaard chillwave.
- Look at me
- I am in a list
- Woo hoo
私は Hipster Ipsum を使用しましたが、何を使用してもかまいません。先頭の front matter には、title と author しかありません。BlogPost
コンポーネントで使用するものは、これらのみだからです。コンポーネントで使用する限り、必要なものは何でも(例えば、日付)、ここに追加できます。
できました!localhost:3000/post/mypost
に移動して、努力の結晶を見てください!
記事のリスト
これで、Markdown のブログ記事にダイナミックルートを設定できるようになったので、作成するブログ記事をブログのホームページですべてリストするようにしてみましょう。components/
ディレクトリに作成済みの PostList.js
があって良かったです!そのファイルに次のコードを書き込みます。
import Link from 'next/link'
export default function PostList({ posts }) {
if (posts === 'undefined') return null
return (
<div>
{!posts && <div>No posts!</div>}
<ul>
{posts &&
posts.map((post) => {
return (
<li key={post.slug}>
<Link href={{ pathname: `/post/${post.slug}` }}>
<a>{post.frontmatter.title}</a>
</Link>
</li>
)
})}
</ul>
</div>
)
}
ここで、prop として渡される記事データの使い方に注目してください。これは index.js
から行う必要があります。そこに戻り、<main>
タグに <PostList />
を挿入します。そして、次のように先頭でインポートします。
+ import PostList from '../components/PostList'
const Index = ({ posts, title, description, ...props }) => {
return (
<Layout pageTitle={title}>
<h1 className="title">Welcome to my blog!</h1>
<p className="description">{description}</p>
<main>
+ <PostList />
</main>
</Layout>
)
}
例えば、記事データなどの外部データを取得する場合、どうしますか?getStaticProps
を使用します!スクロールダウンして、次のように関数を変更します。
export async function getStaticProps() {
const configData = await import(`../siteconfig.json`)
const posts = ((context) => {
const keys = context.keys()
const values = keys.map(context)
const data = keys.map((key, index) => {
let slug = key.replace(/^.*[\\\/]/, '').slice(0, -3)
const value = values[index]
const document = matter(value.default)
return {
frontmatter: document.data,
markdownBody: document.content,
slug,
}
})
return data
})(require.context('../posts', true, /\.md$/))
return {
props: {
posts,
title: configData.default.title,
description: configData.default.description,
},
}
}
Markdown のファイルを取得する posts
変数をここに追加し、(以前と同じように)解析してから、その posts
変数を Index
の props に渡します。現在、index.js
ファイルの残りの部分は次のようになっています。
import matter from 'gray-matter'
import Layout from '../components/Layout'
import PostList from '../components/PostList'
const Index = ({ posts, title, description, ...props }) => {
return (
<Layout pageTitle={title}>
<h1 className="title">Welcome to my blog!</h1>
<p className="description">{description}</p>
<main>
<PostList posts={posts} />
</main>
</Layout>
)
}
export default Index
matter
をインポートして front matter を解析しました。posts
を prop として渡してから、それをドリルダウンして PostList
にしました。これで実際の記事のリストが得られます。ブラウザを見てみましょう!
やりました!!記事のリストがあります!!これで、Markdown のファイルを posts
ディレクトリに追加すると必ず、そのファイルがリストに表示されます。
オプションのカスタマイズ
今はスタイルにまで踏み込みませんが、スタイルが組み込まれたデモの最終版をご覧になりたい場合はこちらへどうぞ!Next 9.4 では、styled-jsx、Sass、CSS モジュール、そしてすぐに使えるその他の機能がサポートされています。好きなものを何でも使用できます。
Next 9.4 では、コードに absolute import を追加する機能をリリースしました(これについては、こちらのブログ記事で詳しく説明しています)。インポートをクリーンアップしたい場合(その ../../etc
をすべて記述したい人は誰もいません)、jsconfig.json
をプロジェクトの最上位に追加できます。
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@components/*": ["components/*"],
"@utils/*": ["utils/*"]
}
}
}
これで、コンポーネントにアクセスするときはいつでも、../../components/something
の代わりに @components/something
を入力できます。デモプロジェクトをチェックして、稼働している様子をご覧ください!
Netlify へのデプロイ
皆さん、長い道のりでしたね。私たちのブログを世界中の人々に披露しましょう。netlify.toml
というディレクトリの最上位にファイルを作成し、次のように入力します。
[build]
command = "npm run build && npm run export"
publish = "out"
ここで、command
は、最終的なサイトのビルドとバンドルに Netlify が使用する「Build Command」を指しています。publish
は、バンドル後にビルドが配置されるディレクトリを指しています。Next.js では、別途設定しない限り、エクスポート先は常に out
ディレクトリになります。
次に、package.json
を開き、scripts
の "start"
の下に以下を追加します。
"export": "next export"
驚くべきことに、プロジェクトを Netlify 向けにセットアップするために必要なことは、これだけです!コードをお気に入りのプラットフォームのリポジトリにプッシュします。
次に、Netlify にログインし、[New site from Git](Git の新サイト)をクリックし、表示される指示に従ってリポジトリを選択します。netlify.toml
ファイルは作成済みのため、ビルド設定はすでに入力されているはずです。
ひときわ目立つ[Deploy site](サイトをデプロイ)ボタンをクリックします。数秒でサイトが稼働します!やりました!!そして、プロジェクトに変更を加えて GitHub リポジトリにプッシュする度に、サイトは自動的に再ビルドされます。すばらしいですね。
繰り返しますが、私がまとめた最終バージョン(スタイルを含む)をご覧になりたい場合は、こちらでチェックできます。または、クリック 1 回で直接デプロイできます。
やりました!
おめでとうございます。長い道のりでしたが、Next 9.4 で構築された Markdown ブログが完成しました。ダイナミックルーティングと Next の最新かつ最高の関数が組み込まれており、最適化された静的サイトが構築されています。当然ながら、Netlify にデプロイしたので、アップデートを続ける限り、アップデートされます。
皆さん、よくがんばりました。本日、何かを掴んでいただけたことを願います。ご質問がある場合は、コミュニティで質問してください。回答させていただきます!