以下のチュートリアルをやっていきます
https://nextjs.org/learn/dashboard-app
htmlとJavaScriptがだいたい分かっている人向け記事です
1. 実行/開発環境について
Reactと同様、以下の3つの実行環境が使えます
A: ビルドしてサーバーに置いて実行する
B: 開発用サーバーを立てて実行する
C: オンブラウザで実行する
Aは本番用で、ビルドすればミニファイ済みのコードが手に入りますので本番環境ではこれを使うべきなんですが、わりと面倒なので本番以外では使わないと思います。BはReactの開発用サーバーを立てて使うやり方で、ライブラリをnpm installしておいてimportして使うという書き方が出来るので通常はこれで開発するんじゃないでしょうか。Cはhtmlのscriptタグでインポートして使うやり方で、既存サイトにちょい足ししたい時や今回のようなチュートリアル的用途に適していると思われます
2. React基本編
Next.jsはReactベースのフレームワークなので、基本的な書き方はReactと共通であるようです
2-1. JSXでDOM操作を書く
JSXはJavaScriptの中にXMLを書けるように拡張した仕組みで、これを使うとタグを追加する度にcreateElementしてcreateTextNodeしてappendChildしてsetAttributeをattributeの数だけやって、というのをやらずにタグをそのまま書くことが出来ます
チュートリアルページに分かりやすい図画ありましたので貼っておきます
例えば以下のような感じで書くことができます
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById('app');
ReactDOM.render(<h1>Develop. Preview. Ship. 🚀</h1>, app);
</script>
</body>
</html>
scriptタグでreactとreact-domとbabelをインポートしてReactDOMとJSXが使えるようにして、ReactDOM.render()に書いた内容をid="app"であるタグに追加しています
上記htmlファイルをブラウザで開くと以下のように表示される筈です
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Fcd818155-82a9-3762-dd39-3d12e30b80f2.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=d6c95034150de043768a7082dbafd58a)
2-2. 関数で書く
タグ名の書き出しを大文字にすると関数を実行してくれます
関数から更に関数を呼ぶこともできますので、機能ごとに記述を分けて可読性を高めることができます
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById('app');
function Header() {
return <h1>Develop. Preview. Ship. 🚀</h1>;
}
function HomePage() {
return (
<div>
{/* Nesting the Header component */}
<Header />
</div>
);
}
ReactDOM.render(<HomePage />, app);
</script>
</body>
</html>
2-3. 変数を使う
関数ですので引数を渡すこともできます
以下ではtitle="React 💙"を引数として渡しています
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById('app');
function Header(props) {
return <h1>{props.title}</h1>
}
function HomePage() {
return (
<div>
<Header title="React 💙" />
</div>
);
}
ReactDOM.render(<HomePage />, app)
</script>
</body>
</html>
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F0988f0aa-a583-4b0b-e9de-7b450c5b721b.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=127f3d4fd4dc7a374da6221ddb5248d8)
引数はオブジェクトとして受け渡されるので、変数に直接代入していくこともできます
function Header({ title }) {
return <h1>{title}</h1>;
}
テンプレートリテラルや関数を呼ぶ書き方もできます
function Header({ title }) {
return <h1>{`Cool ${title}`}</h1>;
}
function createTitle(title) {
if (title) {
return title;
} else {
return 'Default title';
}
}
function Header({ title }) {
return <h1>{createTitle(title)}</h1>;
}
条件演算子も使えます
function Header({ title }) {
return <h1>{title ? title : 'Default Title'}</h1>;
}
2-4. 繰り返し処理
map()のarrow関数の中にXMLを書くと順番に置いていってくれます
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById('app');
function Header({ title }) {
return <h1>{title}</h1>;
}
function HomePage() {
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
return (
<div>
<Header title="Develop. Preview. Ship. 🚀" />
<ul>
{names.map((name) => (
<li>{name}</li>
))}
</ul>
</div>
);
}
ReactDOM.render(<HomePage />, app)
</script>
</body>
</html>
「え?どういう仕組みでそうなる??」と戸惑いながらも便利さに震えます
2-5. eventListenerを設定する
htmlに直接書くときと同じようにonClick={}でeventListenerを追加できます
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById('app');
function Header({ title }) {
return <h1>{title}</h1>;
}
function HomePage() {
function handleClick() {
console.log('increment like count');
}
return (
<div>
<button onClick={handleClick}>Like</button>
</div>
);
}
ReactDOM.render(<HomePage />, app)
</script>
</body>
</html>
生成されたhtmlを見るとonClickは書かれていません
あくまでそういう書き方をするだけで、JSXが解釈してaddEventListenerしてくれるようです
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F88320d3c-2c0b-15e8-5f87-c14664f55445.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=8da0e0bf947ebf4bb36208ed03d04567)
2-6. useStateを使う
JavaScriptでUIを動かすときにはパーツごとに状態を表現する変数をもたせておいて、変数に更新があったら自動的に再描画するというのをやりますが、ReactのuseStateを使うとこれを自動でやってくれます
以下ではlikesという変数を保持しておいて、setLikes()で値が更新される度に再描画するように書いています。React.useState()がこの変数と変数更新用の関数を作ってくれます
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app = document.getElementById('app');
function Header({ title }) {
return <h1>{title}</h1>;
}
function HomePage() {
const [likes, setLikes] = React.useState(0);
function handleClick() {
setLikes(likes + 1);
}
return (
<div>
<button onClick={handleClick}>Likes ({likes})</button>
</div>
);
}
ReactDOM.render(<HomePage />, app)
</script>
</body>
</html>
https://beta.reactjs.org/learn/render-and-commit
https://beta.reactjs.org/learn/referencing-values-with-refs
https://beta.reactjs.org/learn/managing-state
https://beta.reactjs.org/learn/passing-data-deeply-with-context
https://beta.reactjs.org/apis/react
3. Next.js基本編
ここからようやくNext.jsを使っていきます
3-1. 開発用サーバーのセットアップ
nodejsが入っている環境の適当なフォルダでnpx create-next-appを実行すればプロジェクトを作成できます。プロジェクト名でフォルダが作成されるので適当な名前をつけて、TypeScriptとESLintはどちらも有効にしておきます
> npx create-next-app
Need to install the following packages:
create-next-app@13.0.2
Ok to proceed? (y) y
√ What is your project named? ... my-app
√ Would you like to use TypeScript with this project? ... No / Yes
√ Would you like to use ESLint with this project? ... No / Yes
Creating a new Next.js app in C:\Users\jjaka\マイドライブ\python\_blog\react2\my-app.
最低限の構成までは自動で作ってくれますので、ここで開発用サーバーを起動すればサンプルページが http://localhost:3000/ に表示されます
npm run dev
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F60910936-1a54-e0b5-5298-836d0f94221a.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=1b11e9d721245842eaaeaf01a3a7d720)
3-2. コードをNext.jsに書き換える
先ほどのnpx create-next-appで作ったプロジェクトフォルダにはフォルダやファイルがあれこれ作られています。Next.jsはpagesフォルダ内にあるフォルダ名をPATHとして使うので、pages/index.tsxがトップページになります
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F290df510-941c-784d-b718-a778db6f2fc1.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=984cb995e173963d04822bc0dfb011e2)
先ほどまで書いていたコードと同じものを作ってみましょう
内容はほぼ同じですが、react.useStateをimport { useState } from 'react'で読み込んでいるのと、ページを書き出す元関数をexport defaultにしているところが変更点です
import { useState } from 'react';
function Header({ title }) {
return <h1>{title ? title : 'Default title'}</h1>;
}
export default function HomePage() {
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
const [likes, setLikes] = useState(0);
function handleClick() {
setLikes(likes + 1);
}
return (
<div>
<Header title="Develop. Preview. Ship. 🚀" />
<ul>
{names.map((name) => (
<li key={name}>{name}</li>
))}
</ul>
<button onClick={handleClick}>Like ({likes})</button>
</div>
);
}
上記で書き換えると生成されるhtmlが以下のようになります。headタグやbodyタグについては何も書いてないんですが適宜やってくれるようです
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F929f8f5f-ef5a-531c-db8b-73173b3a9c74.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=d7dea3fd7c5a231c826eead7d718aeff)
表示されるのがこれですね
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Ffa5418a1-3560-92fb-0c59-5760698877c3.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=9c5139ab51caa0fbf1908b770fc3ace3)
3-3. ページを追加する
pages/posts/first-post.tsx を追加して以下のように記入すると http://localhost:3000/posts/first-post に新しいページが追加されます
export default function FirstPost() {
return <h1>First Post</h1>;
}
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Fd7ef33ef-7b17-4d9a-70ef-76af0e68e83e.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=3e9c4b9d9e0304894a143fa2ad336a81)
3-4. リンクを設置する
next/link.Linkを使えばフォルダPATHでリンクを指定することができます
import Link from 'next/link';
export default function FirstPost() {
return (
<>
<h1>First Post</h1>
<h2>
<Link href="/">Back to home</Link>
</h2>
</>
);
}
Back to homeのところにトップページへのリンクが付いています
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F927cad06-d40c-871c-e153-fadfe5430f6c.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=0b371ad166ebb7c3c4cf5004e1781c8e)
3-5. 画像を表示する
publicフォルダ内のファイルであれば、どのページからでも参照することができます
import Image from 'next/image';
const YourComponent = () => (
<Image
src="/images/profile.jpg" // Route of the image file
height={144} // Desired size with correct aspect ratio
width={144} // Desired size with correct aspect ratio
alt="Your Name"
/>
);
3-6. headerを書く
next/head.Headを使うとheadタグ内の項目を書くことができます
以下ではheadタグ内にtitleを書いています
import Link from 'next/link';
import Head from 'next/head';
export default function FirstPost() {
return (
<>
<Head>
<title>First Post</title>
</Head>
<h1>First Post</h1>
<h2>
<Link href="/">← Back to home</Link>
</h2>
</>
);
}
3-7. scriptの読み込み
next/script.Scriptを使うと読み込みのタイミングを指定してJavascriptファイルを読み込めます
strategyによって読み込みタイミングが変わり、beforeInteractiveならページがインタラクティブになる前の自分が書いたJavascriptコードが実行される前に実行、afterInteractiveならページがインタラクティブになったすぐ後に実行、"lazyOnload"ならインタラクティブになった後にアイドル状態になったら実行という順番で優先順位をつけて読み込むことができますので、すぐに読む必要がないスクリプトは後回しにする事でページ表示を早めることができます。ボット検出やcookieなどの同意管理などはbeforeInteractive、タグマネージャーや分析系のタグはafterInteractiveにするのが一般的なようです。
読み込み終了後の処理をonLoadに書くことも出来ますので遅延読み込みへの対応もやりやすいようです
import Link from 'next/link';
import Head from 'next/head';
import Script from 'next/script';
export default function FirstPost() {
return (
<>
<Head>
<title>First Post</title>
</Head>
<Script
src="https://connect.facebook.net/en_US/sdk.js"
strategy="lazyOnload"
onLoad={() =>
console.log(`script loaded correctly, window.FB has been populated`)
}
/>
<h1>First Post</h1>
<h2>
<Link href="/">← Back to home</Link>
</h2>
</>
);
}
3-8. cssの適用
cssの適用もスマートにやってくれます
.container {
max-width: 36rem;
padding: 0 1rem;
margin: 3rem auto 6rem;
}
import styles from './layout.module.css';
export default function Layout({ children }) {
return <div className={styles.container}>{children}</div>;
}
import Head from 'next/head';
import Link from 'next/link';
import Layout from '../../components/layout';
export default function FirstPost() {
return (
<Layout>
<Head>
<title>First Post</title>
</Head>
<h1>First Post</h1>
<h2>
<Link href="/">← Back to home</Link>
</h2>
</Layout>
);
}
3-9. global CSS
pages/_app.tsxでCSSを読みこめば、すべてのページに共通して適用できます
import '../styles/globals.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
line-height: 1.6;
font-size: 18px;
}
* {
box-sizing: border-box;
}
a {
color: #0070f3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
max-width: 100%;
display: block;
}
4. SSG, SSR
4-1. SSG (Static Site Generation)
4-1-1. SSGとは
ブログのようにコンテンツがある程度固定的なサイトの場合、サーバー側でhtmlを作成 (pre-rendering) しておいてリクエストが来たときにhtmlを送るだけにしておけば、表示までの時間を最小化することができます。この仕組みをSSG (Static Site Generation) と呼ぶんだそうで、これを使えることがNext.jsを採用したいという理由でNext.jsを選択する人も多いと思います
4-1-2. blogサイト用の記事読み込み機能を追加する
ここではblogを題材に作業しますので、以下からファイル一式をダウンロードしてきてプロジェクトに上書きして使いましょう
https://github.com/vercel/next-learn/tree/master/basics/data-fetching-starter
これで開発サーバーを立ち上げると http://localhost:3000/ に以下が表示される筈です
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F9f8ae355-e608-130c-f0c8-3be319394c10.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=7bff2382204cbccc47305fbf69fef399)
このサイトについてSSGで配信するように変更を加えていきます
まず、各ページの情報を読み取る為にgray-masterをプロジェクトにインストールしましょう
my-app> npm install gray-matter
続いてpostsフォルダにblogの記事データに相当するものを置いていきます
実際の実装では適当なバックエンドに送ってもらうか、ヘッドレスCMSサービスから受け取るかすると思いますが、とりあえずの動作デモ用に用意したものだと思います
---
title: 'Two Forms of Pre-rendering'
date: '2020-01-01'
---
Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
Importantly, Next.js let's you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
---
title: 'When to Use Static Generation v.s. Server-side Rendering'
date: '2020-01-02'
---
We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
You can use Static Generation for many types of pages, including:
- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation
You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
このファイルを読む為の関数を定義します
記事はYAML Front Matterで書かれているので、gray-matterによりparseします
fs と path は Node.js のライブラリでファイルやPATHの操作をやってくれるものです
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const postsDirectory = path.join(process.cwd(), 'posts');
export function getSortedPostsData() {
// Get file names under /posts
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = fileNames.map((fileName) => {
// Remove ".md" from file name to get id
const id = fileName.replace(/\.md$/, '');
// Read markdown file as string
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents);
// Combine the data with the id
return {
id,
...matterResult.data,
};
});
// Sort posts by date
return allPostsData.sort(({ date: a }, { date: b }) => {
if (a < b) {
return 1;
} else if (a > b) {
return -1;
} else {
return 0;
}
});
}
これをindex.tsxから読み出せるようにする為に読み出し用の関数を追加します
getStaticProps()をHome()から呼んでないのにこれで読み込みできるのが何故なのかよく分からなかったので、後日分かったら追記したいです
import Head from 'next/head'
import Layout, { siteTitle } from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'
export default function Home({ allPostsData }) {
return (
<Layout home>
<Head>…</Head>
<section className={utilStyles.headingMd}>…</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<h2 className={utilStyles.headingLg}>Blog</h2>
<ul className={utilStyles.list}>
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
{title}
<br />
{id}
<br />
{date}
</li>
))}
</ul>
</section>
</Layout>
)
}
export async function getStaticProps() {
const allPostsData = getSortedPostsData()
return {
props: {
allPostsData
}
}
}
表示すると http://localhost:3000/ に以下が表示される筈です
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F9183444d-5316-a97a-3b02-7532162176e1.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=944b541c04ed7b0ab7e4bc82fb6687e1)
記事が表示されるようになりました
4-1-3. DBや外部フェッチから取得する
先程は記事をフォルダに置いて扱いましたが、APIエンドポイントから取得する場合はnode-fetchなどを使ってフェッチしてきたものをreturnしてやれば良いようです
import fetch from 'node-fetch'
export async function getSortedPostsData() {
// ファイルシステムのかわりに、
// 外部の API エンドポイントから投稿データを取得する
const res = await fetch('..')
return res.json()
}
DBから読む場合は当該DBのSDKなどを使ってクエリを送って取得したものをreturnします
import someDatabaseSDK from 'someDatabaseSDK'
const databaseClient = someDatabaseSDK.createClient(...)
export async function getSortedPostsData() {
// ファイルシステムのかわりに
// データベースから投稿データを取得する
return databaseClient.query('SELECT posts...')
}
4-1-4. pre-renderingを設定する
Next.jsはデフォルト設定のままだと、すべてのページでpre-renderingしますので、じつは既にSSGとしてデプロイされています。開発環境ではnpm run devする度にpre-renderingが実行されますのでユーザー操作で情報が増えたような場合は一度開発サーバーを止めて再度npm run devする必要があります。実行環境の場合はビルドした際に1度だけpre-renderingされますので、ブログで記事が増えたときに再度ビルドする仕組みを仕込むなどしないといけないと思います
また、ユーザーが押した良いねボタンやコメントなどは即時い反映させたい場合はその部分だけSSRやCSRにしておく必要があります
4-2. SSR
SNSのようにユーザーリクエストがある度に新たにページをrenderingしたい場合はSSRまたはCSRを使うことになります。クライアント側のスペックが低くてクライアント側で処理するのが厳しい場合はSSRが選択されるようです。サーバーサイドでhtmlをrenderingするんであればフロントエンドフレームワークを使わない10年前ぐらいのアプリと同じじゃないかと思ったんですが、部分ごとにSSG/SSR/CSRのどれを使うかを選択できて、それらすべてを同じNext.jsで書けることがメリットになるようです
先程使ったgetStaticProps() ではなく getServerSideProps() を使うとSSRとして実行してくれるようです
例えば先程のindex.tsxの場合は以下のようになります
import Head from 'next/head'
import Layout, { siteTitle } from '../components/layout'
import utilStyles from '../styles/utils.module.css'
import { getSortedPostsData } from '../lib/posts'
export default function Home({ allPostsData }) {
return (
<Layout home>
<Head>…</Head>
<section className={utilStyles.headingMd}>…</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<h2 className={utilStyles.headingLg}>Blog</h2>
<ul className={utilStyles.list}>
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
{title}
<br />
{id}
<br />
{date}
</li>
))}
</ul>
</section>
</Layout>
)
}
export async function getServerSideProps() {
const allPostsData = getSortedPostsData()
return {
props: {
allPostsData
}
}
}
4-3. CSR
CSRで書く場合はレンダリング終了後に必要な情報をフェッチして描画するという従来のSPA的なコードを自分で書く必要があります。React.jsでよく作られるSPAの動作がCSRそのものですので、従来通り書くことでCSRが実現できます
Next.jsとしては、データフェッチ用Reactフックを作成していて使用を推奨しているようです
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetch)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
5. 動的ルーティング
ブログの記事ページのように、動的に作成されるページのURLを指定する為の仕組みが動的ルーティングです。やり方は簡単で、"[id].js"のように[]を含むファイル名をpagesフォルダ内に置くとそこを変数で置き換えたPATHにページをルーティングしてくれます。getStaticPaths()でファイル名を渡して、getStaticProps()で書き出すページの内容を渡すと当該ページを生成してくれるようです
import Layout from '../../components/layout'
import { getAllPostIds, getPostData } from '../../lib/posts'
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
)
}
export async function getStaticPaths() {
const paths = getAllPostIds()
return {
paths,
fallback: false
}
}
export async function getStaticProps({ params }) {
const postData = getPostData(params.id)
return {
props: {
postData
}
}
}
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const postsDirectory = path.join(process.cwd(), 'posts')
export function getSortedPostsData() {
// Get file names under /posts
const fileNames = fs.readdirSync(postsDirectory)
const allPostsData = fileNames.map(fileName => {
// Remove ".md" from file name to get id
const id = fileName.replace(/\.md$/, '')
// Read markdown file as string
const fullPath = path.join(postsDirectory, fileName)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents)
// Combine the data with the id
return {
id,
...matterResult.data
}
})
// Sort posts by date
return allPostsData.sort((a, b) => {
if (a.date < b.date) {
return 1
} else {
return -1
}
})
}
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory)
return fileNames.map(fileName => {
return {
params: {
id: fileName.replace(/\.md$/, '')
}
}
})
}
export function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents)
// Combine the data with the id
return {
id,
...matterResult.data
}
}
これで以下に記事ページができている筈です
http://localhost:3000/posts/ssg-ssr
http://localhost:3000/posts/pre-render
6. 404ページをカスタマイズ
404のページは単に pages/404.tsx を作れば良いようです
export default function Custom404() {
return <h1>404 - Page Not Found</h1>
}
7. APIルート
ウェブAPIをつくることも出来るようです
export default (req, res) => {
res.status(200).json({ text: 'Hello' })
}
http://localhost:3000/api/hello
8. デプロイ
8-1. Vercelでデプロイする
8-1-1. githubにpushする
gitにリポジトリを作ってpushします
git init
git add .
git commit -m 'first commit'
git remote add origin 追加したいリポジトリ名
git push -u origin main
8-1-2. Vercelにデプロイする
Vercelにアクセスしてログインします
https://vercel.com/
ログインしたらcreate new project → GitHub → 先程pushしたリポジトリを選択と進みます
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F907928af-2112-bd32-2930-02088ff02275.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=440a6518879d5e589699031af0bc00aa)
正常にインポートできたら自動でどのフレームワークで作られているか判別してくれます
Deployボタンを押すとビルドがはじまります
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F3ae21c7b-d5c4-c5b8-7c28-af4adcb312d2.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=09a142ffda66cc1af82e93735414d61b)
上手くいったようです
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F9183444d-5316-a97a-3b02-7532162176e1.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=944b541c04ed7b0ab7e4bc82fb6687e1)
8-2. ビルドしてデプロイする
公式チュートリアルではNode.jsでデプロイするやり方も紹介されています
https://nextjs.org/learn/basics/deploying-nextjs-app/other-hosting-options
デプロイする環境にファイル一式を置いて以下を実行します
npm run build
上手くビルド出来たら以下でサーバーを起動します
npm run start
9. ヘッドレスCMSでブログをつくる
microCMSの公式チュートリアルをやっていきます
https://blog.microcms.io/microcms-next-jamstack-blog/
9-1. プロジェクトの作成
適当なフォルダに移動して以下を実行します
npx create-next-app blog-with-microcms
9-2. microCMSアカウントをつくる
microCMSにアクセスします
https://app.microcms.io/
まだアカウントがない人はつくります
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F52505ea4-f2d7-bad5-6cb6-6547ce9d23fe.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=cf0c33037255637549758f397bd81cd8)
9-3. サービスをつくる
サービス名は自分が分かる名前、サービスIDは乱数でもなんでも良いけど他の人がまだ使ってない文字列にします
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Ffceaad07-1dee-0d5a-e960-2a4cfad29f69.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=39afaaf5485d9d22ecc0dc9ef5518b5d)
9-4. APIをつくる
ウェブAPIをつくります
今回はブログなのでブログを選択します
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Fe1f0cc8e-16f7-43ae-cf09-977ce470fdf5.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=fcab64f0c6377da93538aec8f0fd45f7)
できました
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F1ab5098a-7a66-fd0f-285a-ef892f4e56ff.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=1af27bf7e4bd7af3c2985b5f7953b7ee)
サンプル記事が既に作成されていますのでクリックして開くと、内容を編集できるようになっています
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F2c0d281b-403d-06a1-e62a-ff1c58c6ce8a.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=a889343dacf4397f4e7a9b9789f9cbb8)
右上にあるAPIプレビューをクリックすると、microCMSのsdkでの取得例が見られます
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Fa4e9477b-8e6d-6b7a-a996-bf4384ea52d2.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=cd11cc0c48f7815ba9a8dba4c36050e2)
9-5. 記事一覧ページをつくる
APIキーはコンソールの左下に書かれている「1個のAPIキー」をクリックして、COPYボタンをクリックすると取得できます
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F921acf74-666e-f856-b42b-ff61e5a6d1ba.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=0d15d5331fc6577f2bd84cef7765b292)
APIキーがあると記事を自由に読み書き改変できるようになってしまうので、うっかりgithubにアップしてしまわないように.env.localなどのgitがアップロードしないファイルに書いておく必要があります
今回はNext.jsプロジェクトフォルダのトップに.env.localファイルを作ってAPIキーを記入します
API_KEY = 3e80a65f4d6a54f89a65f4d6a54f45f2e8f4
続いて、右上にあるAPI設定を開いてドメインを確認します
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F2a83ad42-d7fe-e0d6-a4c4-a7dfe17bdc07.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=aad13fe360102be9c29d4582f4e40591)
libs/client.jsファイルにmicroCMSクライアントをつくっていきます
serviceDomainは先程確認したドメイン、apiKeyは先程.env.localに置いたAPI_KEYを使います
import { createClient } from "microcms-js-sdk";
export const client = createClient({
serviceDomain: "blog-tutorial-nextjs",
apiKey: process.env.API_KEY,
})
取得した情報をindex.tsxで表示してみましょう。/libs/client.jsからclientをインポートして、getStaticProps()で先程作成したAPIからデータを取得します。endpointはAPIをつくるときに自分で決めたものですがコンソールの左側にある基本情報から確認できます。clientがget()したオブジェクトの内容がどうなっているかはAPIプレビューから確認できます
import Link from 'next/link';
import { client } from '../libs/client';
export async function getStaticProps() {
const data = await client.get({
endpoint: 'blogs',
});
return {
props: {
data,
},
};
};
export default function Home({ data }) {
return (
<div>
{ data.contents.map((content) => (
<li key={ content.id }>
<Link href={`/blog/${content.id}`}>
{ content.title }<br />
</Link>
</li>
))}
</div>
);
};
まだ記事が1つしかないので1行だけ表示される筈です
9-6. 記事ページをつくる
getStaticPaths()でSSGするURLを指定し、getStaticProps()で取得した値を使ってページを生成します
受け取ったhtmlをhtmlとしてそのまま貼り付けたいのでdangerouslySetInnerHTMLで貼っていますが、何故dangerouslyと付いているかというと、ここにJavascriptを仕込んだらコードが実行されてしまうからです。ユーザーから受け取った文字列を放り込んだりするとXSS脆弱性を付いた攻撃が可能になってしまいますので避けるべきですが、今回は自分が用意して自分が書き込んだヘッドレスCMSからの文字列しか来ない筈なので大丈夫だろうという使い方で、microCMSのチュートリアルでもこの書き方になっています
import { client } from '../../libs/client';
//SSG
export async function getStaticProps(context) {
const id = context.params.id;
const data = await client.get({
endpoint: 'blogs',
contentId: id,
});
return {
props: {
blog: data,
},
};
};
export async function getStaticPaths() {
const data = await client.get({ endpoint: "blogs" });
const paths = data.contents.map((content) => `/blog/${content.id}`);
return {
paths,
fallback: false,
};
};
export default function BlogId({ blog }) {
return (
<main>
<h1>{blog.title}</h1>
<p>{blog.createdAt}</p>
<div dangerouslySetInnerHTML={{ __html: `${blog.content}` }}></div>
</main>
);
};
できました
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Fc92ea605-ac7f-f14e-a092-f395f5dedc0c.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=fcef52a91ef935abbefd64f7eab1ddad)
9-7. デザインを整える
microCMSからの取得はちゃんと出来たので、デザインを整えていきます
npm install sass
.main {
width: 960px;
margin: 0 auto;
}
.title {
margin-bottom: 20px;
}
.publishedAt {
margin-bottom: 40px;
}
.post {
& > h1 {
font-size: 30px;
font-weight: bold;
margin: 40px 0 20px;
background-color: #eee;
padding: 10px 20px;
border-radius: 5px;
}
& > h2 {
font-size: 24px;
font-weight: bold;
margin: 40px 0 16px;
border-bottom: 1px solid #ddd;
}
& > p {
line-height: 1.8;
letter-spacing: 0.2px;
}
& > ol {
list-style-type: decimal;
list-style-position: inside;
}
}
import { client } from '../../libs/client';
import styles from '../../styles/Home.module.scss';
//SSG
export async function getStaticProps(context) {
const id = context.params.id;
const data = await client.get({
endpoint: 'blogs',
contentId: id,
});
return {
props: {
blog: data,
},
};
};
export async function getStaticPaths() {
const data = await client.get({ endpoint: "blogs" });
const paths = data.contents.map((content) => `/blog/${content.id}`);
return {
paths,
fallback: false,
};
};
export default function BlogId({ blog }) {
return (
<main className={styles.main}>
<h1 className={styles.title}>{blog.title}</h1>
<p className={styles.publishedAt}>{blog.createdAt}</p>
<div dangerouslySetInnerHTML={{
__html: `${blog.content}`
}}
className={styles.post}></div>
</main>
);
};
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F011fddda-1844-30d2-a869-bb7e11b26a11.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=182d53028de1f16f1160c317bda07968)
9-8. ビルドする
npm run build
9-9. githubにpushする
gitにリポジトリを作ってpushします
git init
git add .
git commit -m 'first commit'
git remote add origin 追加したいリポジトリ名
git push -u origin main
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F61882580-b5a0-5c4c-cf45-6a51353faae9.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=df9e84eec07840c6c50aef8bfeb9eb17)
9-10. Vercelにデプロイする
Vercelにアクセスしてログインします
https://vercel.com/
ログインしたらcreate new project → GitHub → 先程pushしたリポジトリを選択と進みます
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F907928af-2112-bd32-2930-02088ff02275.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=440a6518879d5e589699031af0bc00aa)
正常にインポートできたら自動でどのフレームワークで作られているか判別して表示してくれている筈です。ここでDeployボタンを押すとビルドがはじまるんですが、.gitignoreで.env.localをアップロードしなくした結果としてAPI_KEYが取得できなくなっているので、ここのメニューで環境変数としてAPI_KEYを渡してやります
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F1fe61f65-eab5-97a2-f970-5aef5b2b1d95.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=ccd5b3946cc0ff68d1798c8cb714fc8e)
上手くいったら以下みたいに表示される筈です
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Ff57f1117-8a94-65f8-67d6-390837e275df.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=dc61fcad5aed26d2fbc6cd0df2865554)
左側にトップページが表示されていますが、ここをクリックすると現時点でデプロイされているURLに移動できます。この時点では "https://リポジトリ名.vercel.app" に表示されていますが、自分のドメインで表示したい筈ですので、右側にあるAdd Domainのボタンをクリックしてドメインの指定に進みたいと思います
9-11. microCMSとVercelを結びつける
microCMS側で記事を追加したときに自動でVercelが再ビルドする仕組みがあるので設定していきます
9-11-1. Deploy Hookをつくる
まずVercel側にここをつつけば再ビルドしてくれる、というHookをつくります
Vercel上で先ほど作ったプロジェクトのSettingsを開き、Git欄にあるDeploy Hooksに適当なHook名、Githubのブランチ (通常はmainだと思います) を入力してCreate HookボタンをクリックしたらHookをつくってくれます
9-11-2. microCMSにHookを登録する
API設定のWebhook欄で追加を選び、Vercelを選択、適当なHook名と先程つくったVercelのDeploy Hookを貼り付けます
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F51848c0a-cb6b-c5c3-50c0-e37da7adf595.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=8fb43be69a548420f8bb0587c9388f2d)
microCMSで記事を追加してみると、Vercel側はいじらなくても自動的に再ビルドしてくれました
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F7b2bcd66-471a-50cc-e4ba-b8bf574d1b4f.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=d5b4d13226fcc62c3f02bf4c2c6867a1)
9-12. 独自ドメインでアクセスできるようにする
Vercelの設定メニューでDomainsを選ぶと自分のドメインを設定できます
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Ffd1d79c9-ed4b-f43e-cba5-694504c363f4.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=35189315c66cf2163f612230712230b9)
テキストボックスに設定したいURLを入力してADDボタンを押すとDNSサーバーに設定する内容が表示されますので、これを自分のDNSサーバーに設定すればOKです
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F429baf6e-ef4c-47cc-5205-54538f7ffc8b.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=594845a603045032f0b481c712ddd4f7)
お名前どっとこむの場合はDNS設定からDNSレコード設定に入ってType(AとかCNAMEとか)とValueを記入します
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2Fa12de57d-91c1-c7b7-d5a6-c300a7b415a5.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=38850ac379ea992428e3f6d06037b714)
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F8f1ee9ac-2532-3da4-9af2-b5759d62778a.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=57f92edbac455eb5b8a48440c263e58a)
しばらく待つと、VercelのDomain設定ページで登録したURLが有効になっているのを確認できます
![](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F277233%2F35b89e9c-3974-5429-ca94-ae87c7ab6d90.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&s=789b25679465bbf01c776adf611ad50d)
問題なく表示できるのを確認したら、要らなくなった仮のURLの方を削除して完成です