2020年4月に、DrupalというCMSで構築していたサイトを1からリニューアルしました。Drupalは簡単にいうとWordPressのようなCMSです。テーマで見た目を変えることができ、プラグインをインストールして機能を拡張できます。
リニューアル後は、CMSにはHeadless CMSのContentfulを採用し、Next.jsを使っています。ホスティングはVercelを使いました。
利用技術
- Next.js
- TypeScript
- Material-UI
- Vercel
- Contentful
リニューアルしたサイト:週末養蜂の始め方 養蜂の方法を紹介するサイトです。月間約30万PV
モダンな技術の恩恵を受けることができ、以下のようなメリットがありました。
- サイトが落ちる心配がなくなった
- サーバのパッケージ更新やSSL更新などの手間がなくなった
- マークダウンで記事が書けるようになった
- Lighthouseの速度スコアが大幅に上がった
- サイトの遷移が超高速になった
- 細かいUIの変更が簡単にできるようになった
- 社内の利用技術をReactに統一したため、リソースの割り当てがやりやすくなった
Next.jsはなかなか良いですが、日本語では情報がないので参考になれば嬉しいです。基本的な内容についてはドキュメントや他の方が作られたまとまった記事を参考になるので、この記事では実際に困ったところや運営する上で工夫したところを中心に書きました。
また、Next.jsの実装の大部分は、WALOVERのsanimanさんに行っていただきました。
移行の背景
より力を入れてサイトのコンテンツや速度を改善していこうと考えていましたが、構築から10年近く経ち、利用技術はかなり古くなっており、様々な問題と不満がありました。そこで、このタイミングでリニューアルすることになりました。
1. 脱サーバしたかった
これまではCMSのDrupalで構築していました。月間30万PV程度なのでサーバーが1台あれば普段は十分ですが、テレビで取り上げられたときはPVが通常の100倍になったこともありました。
AWSのEC2を使っていたのですが、もっとアクセスが増えてきたらサーバを2台にして、ロードバランサーで、、、のような面倒なことはやりたくありません。
2. 保守性を高めたかった、高速化したかった
少しサイトをカスタマイズしたいと思っても、DrupalのCMSではプラグインやテーマなど構成が複雑ですし、jQueryが使われており変更が容易ではありませんでした。
また、昨今はサイトの高速化が求められていますが、アクセスされるたびにサーバーサイドの処理が実行され、データベースに記事を取得していく仕組みでは限界があります。
DrupalもWordpressのようにウェブの管理画面上から記事の投稿や様々な設定ができますが、世界中で使われているため、攻撃されることもあります。
3. 利用技術をReactに統一したかった
現在、少人数で複数のアプリケーションの開発をしています。どれも小規模なアプリケーションです。少しずつ機能追加が発生したり、突発的なバグ修正を行う機会が多く、複数のアプリケーションのコードを触ることも珍しくありません。
このため、利用技術をできるだけ統一して、1人が多くのアプリケーションを開発することで効率化を狙いました。ウェブでReactを使っていましたし、モバイルアプリでもReact Nativeを利用しているので、静的サイトも自然な流れでReactを利用することにしました。
Contentfulについて
以前から気になっていたContentfulというHeadless CMSを利用しています。英語ではたくさん利用事例や実装例が見つかりますし、よく使われているサービスであったこと、無料で使えるので利用しました。実際に、今のところ無料で利用できています。
ただ、ContentfulはUIが英語なので記事を書く人にもある程度の慣れが必要です。また、拡張性が高い分、モデルの定義などが非エンジニアには難しいです。microCMSという国産のHeadless CMSなど、Headless CMSには色々あるので使いやすいものを選ぶと良いと思います。
Contentful上での記事の作成もAPIでの投稿取得も簡単ですが、少し詰まったところを紹介します。
1. カテゴリやタグの設計方法
タグやカテゴリを実現するためには、データベースを設計するように各モデルを設定します。そう難しい内容ではないですが、エンジニア以外の人にはちょっと敷居が高いです。
カテゴリの作り方については、以下のものがよくまとまっています。タグについても同様に設定できます。
【Contentful】カテゴリーモデルとブログ記事モデルの関連付け - 独学プログラマ
2. 通信回数を減らす方法
記事が表示される度にContentfulに通信するのでその分遅くなるのでは?という疑問があるかもしれません。その通りで、Contentfulを使う場合には注意が必要です。実装によってはContentfulに数回リクエストする場面が出てきます。
例えば、記事詳細ページで記事のタイトルや本文の他にも、同じカテゴリの記事なども表示しますが、一度にデータが取れない場合があります。
- 記事のデータを取得
- 1で取得したカテゴリIDをキーにして、同じカテゴリ一覧の記事を取得
- サイトの目次を取得
のように、何度もAPIにリクエストすると、その分遅くなっていきます。Contentfulを使う場合は、リクエスト回数が増えすぎないように工夫が必要です。そこで、ビルド時にのみ通信を行って静的ファイルを生成するNext.jsの機能(後述)を使いました。こうすると記事の表示の時には通信が行われないので、何回も通信しても大丈夫です。
他の方法としては、取得した記事やカテゴリなどのデータをファイルに保存して保持する方法があります。実装例を探した中でこの方法を行っているものを何件か見つけました。ただ、記事のデータが更新される度に手動でデータを更新するか、自動で更新されるように仕組みが必要です。また、ページ数やコンテンツが増えるとファイルがどんどん肥大化して、パフォーマンスが悪化する恐れがあります。
Next.js とは
Next.jsを使うとReactで手軽にサーバーサイドレンダリングを行えます。詳しくは公式サイトをご覧ください。チュートリアルがよくできています。
Githubのスター数は47800とかなり多いですが、日本では情報、採用数がまだ少ないようです。FRIDAYデジタルが公式のshow caseに掲載されていました。
Next.jsでは公式のチュートリアルでルーティングと、サーバーサイドで実行される独自のライフサイクルメソッドを理解すれば実装を始めることができます。普段からReactで開発をしている人にとっては簡単です。
ReactでSSRを行う場合はGatsbyJSも選択肢に入ります。GithubではNext.jsとほぼ同数のスターがついています。GatsbyJSはプラグインやテーマなどを利用して、モダンな技術を利用したサイトを短時間に構築できますが、その分複雑な印象を受けました。
Next.jsの方が学習コストが低いことと、拡張性が高いと判断して選択しました。Next.jsでも静的ファイルとしてサイトを出力することができますし、多少時間がかかったとしてもAMP、PWAなどの静的サイトに必要とされる機能は実装できます。
どちらを選択するかはケースバイケースだと思います。シンプルなサイトをサクッと作るのなら、GatsbyJSの方が効率が良いかもしれません。
Next.jsの実装のポイント
先ほど紹介したように学習コストが低いので普段からReactを触っている人はすぐに実装できます。実際にハマったところ、公式ドキュメントではわかりにくかったところを紹介します。
Next.jsとHeadless CMSを使う記事は日本語では少なく、主に英語で探しましたが、少々苦労しました。Next.jsのバージョンアップが早いので、古いバージョンの実装方法が紹介されていることも多く、特に、実装開始時はver9.0だったのですが、途中でver9.3がリリースされて大きく実装方法が変わりました。
大幅にリニューアルされた Next.js のチュートリアルをどこよりも早く全編和訳しました
この記事を書いている5月12日にver9.4がリリースされて新しい機能が追加されています。
1. TypeScript
他のプロジェクトでもTypescrpitを使っていることもあり、Next.jsでもTypeScriptを使いました。公式ドキュメントの記載の簡単な設定ですぐに対応できます。
2. ルーティングとサーバーサイド処理
Next.jsの大きめ利点はルーティングが簡単なことです。ディレクトリ構成のみでルーティングを設定できます。また、サーバサイドの実装も、Next.js独自のライフサイクルメソッドに記載するだけで簡単に実装できます。
pagesディレクトリとルーティング
pagesという名前のフォルダが特殊な役割を持っています。/faq
にアクセスすると、pages/faq/index.tsx
が呼ばれます。
動的ルーティングは、少し変わった方法ですが[]
で囲ったファイルを用意しておきます(例: [slug].tsx
)。/faq/hoge
にアクセスすると、pages/faq/[slug].tsx
が呼ばれ、hoge
をparams.slug
のように取得できるようになっています。
pagesに配置すると、サーバーサイドで実行される特殊なライフサイクルメソッドを書くことができます。サーバーサイドでparams.slug
の値に応じて記事を取得すれば動的ルーティングができます。
.
├── pages
├── faq
│ ├── [slug].tsx <-- /faq/hoge (動的)
│ └── index.tsx <--- /faq で呼ばれる
├── post
│ └── [slug].tsx <-- /post/hoge (動的)
getInitialPropsはver9.3からは使わない
サーバーサイドで実行されるNext.js独自のライフサイクルメソッドが、getInitalProps
です。ただ、9.3から新しい方法が出てきました。それがgetServerSideProps
または、getStaticProps
です。このため、getInitialProps
を使うシーンは今後ほとんどないと思います。
getInitialProps
は、基本的にはサーバーサイドで呼ばれますが、ブラウザの戻るボタンで戻った時や、あとで紹介するLinkコンポーネントで遷移した場合は、クライアントサイドで呼ばれるという仕様でした。
このためサーバーサイドでもクライアントサイドでも動作するように記述する必要があります。直接ページを開いた時は表示できていても、遷移するとエラーが出てホワイトアウトするなんてことがあり、何度かハマりました。
getServerSidePropsがベター
getServerSideProps
は、サーバーサイドのみで実行されます。この点でgetInitialProps
よりも安心です。
ただ、ページが読み込まれる度にサーバーサイドで処理が実行されるため、contentfulへの通信を繰り返した場合は、サーバのレスポンスが若干遅くなります。
現在、getServerSideProps
を使っているのは、検索ページだけです。search?q=キーワード
のようにURLパラメータを渡して、アクセスするたびにサーバーサイドでContentfulの検索APIを叩いています。それ以外は、getStaticProps
を使っています。
静的ページではgetStaticPropsがベスト
本番環境用のビルド時にされる関数で、今回でいうとcontentfulのような外部データをビルドに取得して静的ファイルを生成してくれます。今回のような静的サイトの場合は、ほとんどのページがgetStaticProps
を使ってビルド時に生成することで高速化が可能です。
以下は実際のコードを簡略化したものです。
export async function getStaticProps({ params }) {
// slugを取得 /faq/hoge でアクセスした場合の値は hoge
const slug = params.slug
// FAQページの本文データを取得
const faq: FAQ = await getFAQBySlug(slug)
// 同じカテゴリのFAQを取得
const faqs: FAQMeta[] = await getFAQByCategoryId(faq.meta.category.id)
return {
props: { faq, faqs},
}
}
動的ページの場合は、ビルド時に実行するパスを指定します。それがgetStaticPath
です。すべての投稿を取得して配列を作成しまし。
export async function getStaticPaths() {
const faqs = await getAllFAQ()
const paths = faqs.map(faq => `/faq/${faq.meta.slug}`)
return { paths, fallback: false }
}
最初は、 getInitialProps
で実装していましたが、途中でNext.jsのバージョンが上がったので、getStaticProps
に書き換えました。ただ、書き換え自体はわずかな時間で済みました。
getStaticProps
について詳しくは先に紹介した公式ガイドの和訳が詳しいです。
なお、開発環境ではアクセスするたびにデータを取りに行くので、getServerSideProps
と同じようにすぐに変更が反映されます。
_app.tsxについて
pagesディレクトリの全ページのクライアントサイドで実行される共通処理を書くファイルです。全ページに共通で適用させたい処理を書きます。今回、UIには、Material-UIを使っています。Theme(色や文字スタイル、余白・・・)もこちらで読み込み全てのページに適用させます。
// グローバルCSS
import GlobalStyle from '../theme/GlobalStyle'
Sentry.init({
dsn: process.env.SENTRY_DSN,
})
class MyApp extends App {
componentDidMount() {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side')
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles)
}
}
render() {
const { Component, pageProps } = this.props
return (
<React.Fragment>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<GlobalStyle />
<Component {...pageProps} />
</ThemeProvider>
</React.Fragment>
)
}
}
export default MyApp
_document.tsxについて
pagesディレクトリの全ページのサーバーサイドでのみ実行される共通処理を書くファイルです。サーバーサイドで処理して展開するCSSの設定はここに書く必要があります。例えば、グローバルCSSを_app.tsxに書いてもこちらの_document.tsxにも処理を描いてあげないとサーバーサイドで展開してくれません。
3. Next.jsとMaterial-UI
ReactのUIフレームワークにはいくつも選択肢がありますが、今回は、Material-UIを採用しました。Googleの提唱するMaterialDesignに沿ったコンポーネントを使うことができるライブラリです。
Next.jsではstyled-jsxのサポートが組み込まれており、コンポーネント内にscopedなCSSを書くことができます。
ですが、当サイトではUIフレームワークにMaterial-UIを採用していることもありほとんどNextのstyled-jsxは使用していません。合わせてstyled-componentも使用しておりグローバルCSSなどもstyled-componentを使って記述しました。
Typescriptもサポートされていて、Next.jsとTypeScriptで作る際にも親和性が高く、コミュニティも充実しているイメージです。他のプロジェクトでも利用していたためMaterial-UIありきで進めました。
Next.jsへの導入
NextJSとMaterial-UIを併用して使う際はこちらのレポジトリを参考に書くことが多いと思います。ここで肝になるのは_app.tsxと_document.tsxの設定の部分かと思います。
Material-UI公式から提供されているNextとの構築例
Material-UIとTypescript
Material-UIと、TypeScriptとの相性については全く問題なく使えています。TypeScript + Material-UI v4 のスタイル付きコンポーネント作成ガイドが参考になります。
当開発当時すでにver.4がリリースされていましたが、公式サイトのドキュメントも揃っていない状況などもありwithStyleのところなどver3の書き方のままになって今にいたっています。ver.3で実装していた他のプロジェクトを先日ver.4にアップデートしたところTypeScriptの型のチェックによるエラーで動きませんでした。ver.4になってTypescriptの型チェックなども厳密になったようです。Matarial-UIもアップデートが比較的早いので更新していく必要があります。
軽量なフレームワークの選定もあり
今回はシンプルな構成のサイトとしては、Material-UIは高機能すぎたという印象があります。高機能になると必然とバンドルサイズや読み込むCSSの記述量も多くなりがちです。必要最低限のミニマムなフレームワーク選定でも良かったかな思いました。
これは、サイトを構築していく過程でLighthouseによるサイトのパフォーマンススコアがだんだん悪くなったためです。高速化を考えると、JSのバンドルサイズやCSSの記述量などを考慮する必要が出てきます。最近、他のCSSライブラリをみている中でTailwindCSSというライブラリをよくみるようになってきました。TailwindCSSのようなシンプルなフレームワークの検討もありだと思います。
4. Linkコンポーネント
Next.jsには、Link コンポーネントという便利なコンポーネントがあり、サイト内リンクを高速に遷移できます。簡単な記述でサイト内遷移を高速化できるのでとても魅力的です。週末養蜂の始め方の目次や記事下のリンク、FRIDAYデジタルのリンクも、多くが一瞬で遷移できるので試してください。
Linkコンポーネントの部分が描画されたタイミングで、リンク先のページのファイルを読み込んでいます。クリックすると、通信することなく一瞬でページが切り替わります。通信処理などは一切書かずに済むので楽々です。ただ、Linkコンポーネントはハマりどころがいくつかあり、ドキュメントをちゃんと読む必要があります。
まず、動的ページの場合は少しややこしいですが、href
とas
を記載します。最初はas
の書き方が分からずにハマりました。
// /faq/hoge へのリンク
<Link href={`/faq/[slug]`} as={`/faq/hoge`} key={key}>
...
<Link/>
もう一つの落とし穴は、Linkの直下にaタグがない場合の動作です。例えば、Material-UIでは、<ListItem button component="a">
のようにかくと、リンクとして機能させることができます。しかし、aタグのラッパーコンポーネントという位置付けでNextのLinkの直下にaタグを置く場合と違って自動でhrefを付与できません。AMPで書き出し場合にAMPでは許可されたJSしか読み込まれないのでNextのルーティングが効かずに、AMPページではaタグにもhrefがない状態になってリンクが反応しなくなります。
Next.jsのLinkコンポーネントにpassHrefというプロパティを書く必要があります。正しい記述は以下になります。
import Link from 'next/link';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
function ListWithNextLink() {
return (
<List>
<Link href="/about" passHref={true}> //このpassHrefが重要です。
<ListItem component="a">
...
</ListItem>
</Link>
</List>
);
};
export default ListWithNextLink
公式の記事ではこちら説明しており、こちらにも同じような内容があります。こちらでは、日本語でQiita内でLinkタグについて詳しく書いてくれています
5. デプロイはVercelを使ってGithubとContentfulと連携
Next.jsを開発している会社が運営しているのがVercelというホスティングサービスです。ついこの4月まではZeitという会社が運営するNowというサービスだったのですが、今はVercelという名前に変わっています。月間PVが30万程度と少ないこともあり、あまり考えずに試したところ使いやすかったのでそのまま使っています。月額30ドルで運用できています。
VercelとGithubを連携すると、masterブランチにpushすることでデプロイできます。また、プルリクを送ったときにも、プルリクの確認用のビルドを自動でデプロイして、本番とは別のURLで確認できるようにしてくれます。プルリクチェックの動作確認のために自分でビルドしなくても良いので便利です。
さらに、Vercelはビルド用のウェブフックを提供しているので、Contentfulで記事を更新したときに、Vercelのウェブフックを叩くように設定しておくと、記事を更新するたびに再ビルドできます。
Next.jsはver9.3で新しくgetServerSideProps
やgetStaticProps
が登場しており、今も活発に開発されていて、これからも仕様が変わる可能性が高いとみています。ホスティングと開発元が同じなのは安心なのでしばらくは使い続けるつもりです。
なお、上には書きませんでしたが、Next.jsの場合でも、完全な静的なファイルを生成する機能があります。Static HTML Export という機能です。高速化のため途中まで調査、検討していましたが、ver9.3でgetStaticProps
が出てきたのでそれを使うことにしました。
エクスポートすると、Netlifyなどのサービスも含めて、どこでもホスティングできるメリットがありますが、実装面の制約やビルドの手間が増えるデメリットがあり、今の段階では選択しませんでした。
マークダウンで記事作成
記事はマークダウンで記述して、それをNext.jsで変換して表示する方式にしました。CMSのDrupalを使っていたときには本文中にリンクやリスト、画像などのhtmlが投稿本文に含まれてしまい、保守性が下がっていました。Drupalでも最初から考えてやっておけばそのような事態は避けれたとは思いますが、この機会に負の遺産も精算することにしました。
記事の移行方法
今回は記事の単純な移行ではなく、SEOの観点から修正、追記も合わせて行いました。このため、手作業でコピーして移しました。もともと150ページくらいしかなかったので、コツコツやっていきました。
マークダウンの変換処理
マークダウンの変換には、markdown-itを使いました。記事取得からhtmlに変換するところまでをgetStaticProps
に記載してビルド時に生成します。propsに変換したhtmlを渡してdangerouslySetInnerHTML
を使って表示させています。名前からしてよくない方法なのかと思いますが、今回は入力値がContentfulからの投稿なのでこのまま使っています。
<Typography
variant="body1"
component="div"
dangerouslySetInnerHTML={{ __html: html }}
/>
記事の効率性・保守性を上げるため、markdown-itでカスタムタグをいくつか定義して使っています。markdown-itの拡張でショートコードを定義できるmarkdown-it-shortcode-tagを利用しています。
内部リンク
記事のURLの形式は、通常記事は/post/slug
、FAQページは/faq/slug
の形式です。
内部のリンクのタイトルやサムネイルをいちいち書くのは面倒で、記事をタイトルを変更したときに保守が追いつかなくなります。<interlink type="post" slug="hoge" card/>
のように、記事タイプ(type)とslugを設定すると展開されるようにしました。
<interlink type="post" slug="hoge" />
のように、cardを記載しない場合は、テキストリンクになります。
この方法だと、内部リンクでありながらLinkコンポーネントが使えておらず、通常の遷移になってしまうのが残念なところです。当初、Next.jsをあまり理解できていなかったのが原因でした。いずれ修正したいと思っています。
独自の画像タグ
画像のホスティングのパスをソース内に保持することで、画像ファイル名だけで表示されるようにしました。また、サイト内で利用する画像はサイズが固定されている場合がほとんどです。このため標準のサイズであればwidth
とheight
の指定を省略できるようにしました。
通常ページの画像もamp-img
を使い遅延ロードしています。また、webpも配置することで、対応ブラウザではwebpが読み込まれるようにしています。
// 画像
// 標準サイズの画像 <myimg s="test.jpg" />
// 標準サイズ以外 <myimg s="test.jpg" w="300" h="400" alt="画像の説明" />
myimg: {
render: attr => {
const defaultWidth = 800
const defaultHeight = 600
let width = defaultWidth
if (attr.w) {
width = attr.w
}
let height = defaultHeight
if (attr.h) {
height = attr.h
}
const alt = attr.alt ? attr.alt : ''
return `<amp-img src="${systemConfig.imageHostRoot}webp/${attr.s}.webp" width="${width}" height="${height}" layout="responsive" alt="${alt}"><amp-img fallback src="${systemConfig.imageHostRoot}${attr.s}" width="${width}" height="${height}" layout="responsive" alt="${alt}"></amp-img></amp-img>`
},
},
独自のYouTubeタグ
YouTubeの動画も簡単に埋め込めるようにしました。読み込み速度を高めるために、通常のページでもamp-youtube
を利用して遅延ロードしています。
// 自分の動画 <youtube v="jkadao"/>
// 他人の動画 <youtube v="jkadao" other />
youtube: {
render: attr => {
const id = attr.v
let tag = `<amp-youtube layout="responsive" width="640" height="390" data-videoid="${id}"></amp-youtube>`
if (!attr.other) {
tag += `<small>多数の養蜂の動画をYouTubeで公開しています。チャンネル登録で応援お願いします。
<a href="${siteInfomation.youtubeSubConfirmationUrl}" target="_blank" rel="noopener" onclick="ga('send', 'event', 'YouTube', 'sublink_click', '${id}' );">こちら</a></small>`
}
return tag
},
},
自分のYouTubeの動画の場合は、チャンネル登録へのリンクが動画の下に自動的に表示されます。
季節に応じた表示の自動切り替え
これまでは季節に応じた表示・案内を手動で掲載していました。例えば、「春は〇〇してください」、「もうすぐ冬なので〇〇しましょう」というような内容です。それぞれの記事でも、読まれる季節によって違う内容を伝えたい場合がありますが、手動では対応しきれずにそのままになっていました。そこで以下のように表示期間を指定できるタグを作りました。
<showdate start="01-01" end="01-31">
1月1日から1月31日まで表示(毎年)
</showdate>
getStaticProps
の中でマークダウンの変換を行なっているため、ビルド時にしかマークダウンの変換処理が走りません。そこで、毎日深夜にVercelのビルド用のウェブフックをcurlで叩いて、内容が更新されるようにしています。
Next.jsのAMP対応
NextJSはAMPを正式にサポートしています。AMPオンリーのサイト構築もできるし、通常ページとAMPページを個別に用意するハイブリッドタイプの構築も可能です。一般的にはハイブリッドタイプのサイト構築を採用する案件の方が多いと思われます。今回、ハイブリッドタイプを採用しました。
ただ、Next.jsのAMP対応の理解と基本的なReactの知識の不足などもあり少しイレギュラーな形でのハイブリッド構成になっています。本来ならページコンポーネントでAMPのハイブリッド構成を宣言してNext.jsで用意されたuseAmp
関数を使いAMPページ、通常ページとコンポーネントを出し分けする必要があるところを、propsの受け渡しがうまくできなかった断念しました。参考:next/amp
そこで、pagesのなかにampフォルダを作ってAMPページを作り、通常ページを<link rel="amphtml" href="AMPページのURL">
で繋げるようにしてamp対応を行いました。参考:AMP公式ドキュメント
.
├── pages
│ ├── amp <- ここにAMP用のファイルを構築(この中身はamp-onlyで構成)
│ │ ├── faq
│ │ └── post
│ ├── category
│ ├── faq <- <link rel="amphtml" href="amp/faq/"> へつなげる
│ ├── feedback
│ ├── lp
│ ├── news
│ ├── news-beemap
│ ├── post <- <link rel="amphtml" href="amp/post/"> へつなげる
│ └── search
AMPページで使いたいコンポーネントのうち、AMPでは対応していないコンポーネントはありませんでした。このため、AMPページではコンポーネントを読み込むだけでした。
記事の本文では、すべての画像がamp-img
で、YouTubeの埋め込みもamp-youtube
でもともと記載されています。ボタンが押された時などに、onclick
でGoogle Analyticsにイベントを送信する部分がAMPに対応していなかったので、そこだけ強引に置換して削除しました。他には、生成されたHtmlをAMPに変換するライブラリを使う、markdown-itをカスタマイズしてAMP時には全ての要素をAMPに変換するなど、色々とやり方はあります。
公式に紹介されている、useAmp
で判定しコンポーネントで出し分けするやり方でも、それなりにコードやファイルを記載する必要があります。今回はシンプルなサイトだということもあり、上記の方法がわかりやすく、簡単に実装できました。
Next.jsのPWA対応
PWA対応に関してはこちらの記事を参考にホームに追加のみを対応させました。PWAと言えば、オフラインでもページが見られるようにServiceWorkerでキャッシュをローカルに残して表示を高速化してくれるのですが、今回はこのページキャッシュのところは対応させていません。
キャッシュをローカルにバックグランドで保存して高速で表示させてくれるのはいいのですが、新しく記事を公開や更新した時にはキャッシュを更新させないといけません。その労力に比べて、オフラインで読めることのメリットが小さいと判断しました
ちなみにNext.jsで効率よくPWAを導入するライブラリにはNext-Offlineなるものがあるようです。こちら公式のExampleにも構築例が置いてあります。
こちらのライブラリを使えばキャッシュをうまく更新してくれるサイクルを導入することができるようです。今後本格的にPWAを導入するのであればこちらのライブラリを導入してやることなりそうです。
Next.jsのサイト高速化
Next.jsを利用すれば、getStaticProps
とLink
コンポーネントを使うだけでも早いサイトの部類に入っていると思います。
AMPにも対応しており、体感では十分な速度が出ていると判断したため、あまり本格的には行っていませんが、調べた内容と、高速化に利用できる技術を紹介します。
画像の遅延ロード、Webp対応
マークダウンの項で説明したように、amp-img
を通常ページにも使って遅延ロードしました。また、Lighthouseのアドバイスに従ってWebp画像も用意して、対応デバイスにはWebpが表示されるようにしています。ただ、もともと遅延ロードされている画像のであまり効果の実感はありませんでした。
Material-UIのBoxコンポーネントの書き直し
Material-UIにBoxコンポーネントという柔軟にスタイルを適用できるコンポーネントがあります。
フォントサイズやカラー、マージン・パディングサイズなどほとんどのスタイルをコンポーネントのプロパティとして書いてスタイルを適用できる便利なコンポーネントです。
しかし、このコンポーネントには落とし穴がありました。それはCSSバンドルサイズが重くなってしまうということです。色々なスタイルが使える分、必要のないスタイルの定義まで読み込んでしまいバンドルサイズが増えてしまいます。
サイトのパフォーマンスに影響してしまうため、後から通常のCSS定義に書き直しました。ちなみに、必要なプロパティだけを読み込むという書き方もできるようなのですが、その手間と書き直す手間一緒ぐらいということで結局書き直すことにしました。
検証はしてないのですが、最新バージョンのMaterial-UIではこの問題は解消されていると思います。GithubのIssueにもあがっていて将来的には改善されるだろうみたいに書かれてます
bundle analyzerでの解析
next-bundle-analyzerを使って分析ができます。当初はmomentを使っていましたが、momentだけで半分程度を占めていたためdaysjsに切り替えました。
今回は使っていませんが、Next.jsでは、ComponentやModuleを遅延ロードする仕組みも提供されています。
その他のメモ
補足的な内容をおまけとして紹介します。
Contentfulを利用した検索ページの実装
Contentfulには検索用のメソッドも用意されています。Elastic Searchなどを使うことも検討しましたが、ページ数が数百程度と少ないことを考慮して、Contentfulの機能を使うことにしました。
検索ページはキーワードをユーザーが入力するという性質上、最初から生成して置くことは難しいため、getServerSideProps
でサーバーサイドレンダリングしてデータを表示しています。?q=ニホンミツバチフォーム
のように入力された文字列をServerSidePropsで取得してContentfulにリクエストを行い、結果をリストで表示するようにしています。
https://syumatsu-yoho.com/search
ContentfulにはElasticSearchと連携する機能があるので、より本格的な検索エンジンを実装する場合はこれを使うことになります。
LPをセクション単位のコンポーネントに分けて再利用
pagesフォルダ内にlpフォルダを切って、そこに今までWordPressで別ドメインに制作していたLPページを移行しました。
https://syumatsu-yoho.com/lp/start-kit
LPはセクション単位に分割された構成となっていて、画像や文章が違うだけで構成は似たり寄ったりです。これまで、再利用する場合はHtmlやCSSをコピーしてテキストや画像を変更していましたが、それだと保守性が下がります。
そこで、以下のように、セクションごとにコンポーネント化して、テキストなどの情報をPropsで渡すようにして使いまわせるようになりました。
<FirstView
bgOpacity={0.5}
bgColor="black"
backgroundImage="xxx.jpg"
title={
'これなら簡単!週末養蜂スタートキットで\n趣味の養蜂にチャレンジしよう!'
}
subtitle={
'週末養蜂スタートキットなら、簡単にニホンミツバチの捕獲にチャレンジできます。'
}
link={'https://store.shopping.yahoo.co.jp/syumatsu-yoho/set1.html'}
/>