Nextの続きです。前回はCreate Next App + TypeScript + CSS in JS環境を作りました。今回は以前作ったReactスターターキットをNext.jsで作り直し、挫折していたOGP問題を解決しました!今回は全体にわたる環境設定編です。それではいきましょう!
【目次】
※参考:前回記事
【React】Create Next App + TypeScript + CSS in JS環境を作る - クモのようにコツコツと
※参考:ReactでWebアプリを作るシリーズまとめ
qiita.com
前回のおさらい
Create Next App + TypeScript + CSS in JS環境を作った
別ページにも固有のtitle、descriptionを設定できた!
※参考:前回記事
【React】Create Next App + TypeScript + CSS in JS環境を作る - クモのようにコツコツと
これなら以前、Reactスターターキットで挫折した固有のOGP設定もできるのでは!?
※参考:【React】OGPはつらいよ ーSPAでの動的OGP・失敗編ー(Reactアプリスターターキット) - クモのようにコツコツと
作ったもの
ということで作ってみた。
ソースコード
※参考:GitHub - ryo-i/next-app-started
プレビュー
※参考:Nextアプリスターターキット
トップページ
はてなブログでもOGPが表示されとる! next-app-started.vercel.app
Aboutページ
※参考:このアプリについて | Nextアプリスターターキット
Aboutページも固有のOGPが表示される! next-app-started.vercel.app
これまでのスターターキット
Nextスターターキットのベースとなる過去のスターターキットたち。見た目はほとんど変わらない。
Webコーディング スターターキット
ソース
※参考:GitHub - ryo-i/web-coding-getting-sterted: HTML/CSS/JSコーディングの最小環境です。
プレビュー
静的なHTML、CSS、JSのみで書いている。懸念点はページを増やすたびに重複コードのあるHTMLページを増やさなければならない。共通部分の修正はHTMLページ分になる。
※参考:【メタ言語】フロントエンド開発スターターキットを作った(EJS、Sass(SCSS)、TypeScript) - クモのようにコツコツと
フロントエンド開発スターターキット
ソース
※参考:GitHub - ryo-i/front-end-getting-sterted: メタ言語(EJS、Sass(SCSS)、TypeScript)のコンパイル環境です。画像も圧縮します。
プレビュー
EJS + Sass(SCSS) + TypeScript環境で共通部分はモジュール化している。EJSとSassはgulpで、TypeScriptはwebpackでコンパイルする。懸念点はEJSとSassがほとんど同じ構成だが別ファイルのため構成変更時は修正が2箇所になる。
※参考:【メタ言語】フロントエンド開発スターターキットを作った(EJS、Sass(SCSS)、TypeScript) - クモのようにコツコツと
Reactスターターキット
ソース
※参考:GitHub - ryo-i/react-app-started
プレビュー
※参考:Reactアプリスターターキット
React + TypeScript + CSS in JS環境。共通部分はコンポーネントとして1ファイルに書いているため、修正は一箇所で済む。懸念点はSPAのためページ固有のOGPが設定できなかった。
※参考:【React】OGPはつらいよ ーSPAでの動的OGP・失敗編ー(Reactアプリスターターキット) - クモのようにコツコツと
このOGP問題を解決するために今回Next.js環境でスターターキットを作り直した!
環境構築
まずCreat Next App + TypeScript + CSS in JS環境を作る。前回と同じ手順。
※参考:【React】Create Next App + TypeScript + CSS in JS環境を作る - クモのようにコツコツと
Creat Next Appをインストールした直後のコミット
※参考:GitHub - ryo-i/next-app-started at 5dfd72546ec60222518947e79828156e16dfe9f2
TypeScriptとCSS in JS(Styled-components)を追加したコミット
※参考:GitHub - ryo-i/next-app-started at 68b4206cb51944f398066b7f80a9968670ec4ee6
CSSホットリロード設定(Babel)
ローカルで開発モード(npm run dev
)で修正しているとCSSの変更が反映されなかった。修正のたびにStyled-componentsから別のclass名が振られるためだった。
※参考:Next.jsでclassNameが見つからなくなるバグの対処方 - Qiita
.babelrcファイルを追加してBabelに下記の設定を追加したら反映されるようになった!
{ "presets": ["next/babel"], "plugins": [ [ "styled-components", { "ssr": true, "displayName": true, "preprocess": false } ] ] }
※参考:Warning: Prop `className` did not match. · Issue #7423 · vercel/next.js · GitHub
OGP設定
静的ファイルを入れる「/public」フォルダを作成しOGP設定に必要な画像ファイル類と「manifest.json」を追加
画像は前回のSPAのOGP設定のときと共通
※参考:【React】Create Next App + TypeScript + CSS in JS環境を作る - クモのようにコツコツと
manifest.jsonの設定
{ "short_name": "next-started", "name": "next-app-started", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "android-chrome-192x192.png", "type": "image/png", "sizes": "192x192" }, { "src": "android-chrome-512x512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" }
short_name
とname
の値を打ち替えている。
「/data」フォルダを作成し「data.json」を追加
{ "head": { "url": "https://next-app-started.vercel.app/" }, // 後略
head
キーにデプロイ先のURLを設定
「/components」フォルダを作成し「CommonHead.tsx」を追加
data.json
をインポートし、変数url
でデプロイ先のURLを取得
import Data from '../data/data.json'; const url = Data.head.url;
CommonHead
コンポーネントでOGPの共通部分を設定(URLにはurl
を設定)
// Component function CommonHead() { return ( <> <meta property="og:url" content={url} /> <meta property="og:image" content={url + "ogp.png"} /> <meta property="og:type" content="website" /> <meta name="twitter:site" content="@idr_zz" /> <meta name="twitter:image" content={url + "ogp.png"} /> <meta name="twitter:card" content="summary_large_image" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <link rel="manifest" href="/manifest.json" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="manifest" href="/site.webmanifest" /> <meta name="msapplication-TileColor" content="#da532c" /> <meta name="theme-color" content="#ffffff" /> </> ); }
CommonHead
コンポーネントをエクスポート
export default CommonHead;
「/pages」フォルダの中に「_app.tsx」を追加。初期設定をカスタマイズできる。
※参考:Advanced Features: カスタム`App` | Next.js
HeadコンポーネントとCommonHead
コンポーネントをインポート
import Head from 'next/head' import CommonHead from '../components/CommonHead';
Head
でCommonHead
を読み込む
function MyApp({ Component, pageProps }) { // 中略 return ( <> <Head> <CommonHead /> </Head> // 中略 <Component {...pageProps} /> </> ) } export default MyApp
Component {...pageProps}
の部分
pagePropsはデータ取得メソッドの 1 つによってプリロードされた初期 props を持つオブジェクトです。そうでなければ空のオブジェクトになります。
※参考:Advanced Features: カスタム`App` | Next.js
共通CSS設定
「/styles」フォルダを作成し「variables.json」を追加
{ "variable": { "baseColor": "#A63744", "textColor": "#333", "textColor_w": "#fff", "bgColor_g": "#eee", "textSize": "14px" } }
使い回すCSSの値を設定する
「/styles」フォルダに「mixin.ts」を追加
import styled, { css } from 'styled-components'; const pageSize = css` width: 100%; max-width: 1000px; margin: 0 auto; `; export {pageSize};
ページサイズの共通CSS、pageSize
コンポーネントを設定
「_app.tsx」でcreateGlobalStyle
、pageSize
、cssVariables
をインポート
import { createGlobalStyle } from 'styled-components'; import { pageSize } from '../styles/mixin'; import cssVariables from '../styles/variables.json';
変数variable
でcssVariables
のCSS変数を取得
const variable = cssVariables.variable;
変数GlobalStyle
でグローバルCSSを設定(variable
やpageSize
を読み込む)
// Style const GlobalStyle = createGlobalStyle` body { margin: 0; padding: 0; font-family: sans-serif; font-size: ${variable.textSize}; color: ${variable.textColor}; *, *:before, *:after { box-sizing: border-box; } a { color: ${variable.baseColor}; &:hover { opacity: 0.8; } } section { margin: 30px 0; } h1, h2, p, figure, ul, table { margin: 0 0 10px; } h1, h2 { line-height: 1.25; } p { line-height: 1.75; margin: 0 0 10px; } } main { ${pageSize} padding: 50px 50px 0; @media(max-width: 600px) { padding: 30px 30px 0; } h1 { font-size: 1.5em; } h2 { font-size: 1.25em; color: ${variable.baseColor}; } } `;
styled-componentsでのmedia-query設定は入れ子でOKだった。
MyApp
でGlobalStyle
を読み込む
function MyApp({ Component, pageProps }) { // 中略 return ( <> // 中略 <GlobalStyle /> <Component {...pageProps} /> </> ) }
FOUC対策
ローカル環境では気がつかなかったのだが、クラウド環境(Vercel)にデプロイしたらCSSが一瞬まったく当たらない状態がチラついて表示される。これは「FOUC(Flash of unstyled content)」というらしい。
※参考:CSSのロードタイミングの不備が引き起こす、FOUCとは? - ふろしき.js
ローカル環境ではheadタグの中に<style data-next-hide-fouc="true">body{display:none}</style>
というタグが追加されるためFOUCにならない。しかしクラウド環境ではこのタグは欠落してしまう。
※参考:next.js 🚀 - 開発サーバーにスタイルなしコンテンツのフラッシュがあります(FOUC) | bleepcoder.com
こちらの事例は「CSS Modules」だが「_document.js」に空のscriptタグ<script></script>
タグを追加したら解決したと(自分の環境ではうまくいかなかった)
※参考:Next.js + CSS ModulesでFOUC(CSSの適用遅れによるちらつき)が発生したときの暫定対策
「_document.js」はNext.jsのhtmlタグ、bodyタグをカスタマイズできるファイル。
※参考:Advanced Features: カスタム `Document` | Next.js
自分の場合はこちらの方法のほうがうまくいった。
※参考:next.js 🚀 - 追い風+ cssモジュールを使用した開発でちらつくスタイル | bleepcoder.com
※参考:Development server has a flash of unstyled content (FOUC) · Issue #13058 · vercel/next.js · GitHub
しかし、componentDidMount()
はクラスコンポーネントでのライフサイクルで、自分の場合は関数コンポーネント。
フックの場合はtypeof window !== "undefined"
というif文の判定を使う方法もあるようだ。
※参考:Next.jsで"document is not defined." "window is not defined."のエラーが出たときの対処法 - Qiita
また、フックでcomponentDidMount()
に代わるライフサイクルはuseEffect()
もある。
※参考:【React】フックとjson-serverをFetch APIで連携(Reactライフサイクルの理解) - クモのようにコツコツと
「/styles」フォルダに「styles.css」を追加
.no-fouc { visibility: hidden; opacity: 0; } .fouc { visibility: visible; opacity: 1; }
.no-fouc
では非表示、.fouc
では表示、という設定
この設定、当初は「_app.tsx」のGlobalStyle
の中に書いていたがそれだとFOUCのままだった。
前回のCreate Next AppではFOUCが起こらないのだが、こちらCSSは「/styles」フォルダの中にcssファイルとして設定されていたので、同じ方法をとってみた。
※参考:ようこそ、ねくすと・じぇいえすへ
「/pages」フォルダに「_document.tsx」を追加
import Document, { Html, Head, Main, NextScript } from 'next/document'; class MyDocument extends Document { static async getInitialProps(ctx) { const initialProps = await Document.getInitialProps(ctx); return { ...initialProps }; } render() { return ( <Html className="no-fouc"> <Head /> <body> <Main /> <NextScript /> </body> </Html> ); } } export default MyDocument;
基本的にNextリファレンスに従った内容だが一点、Html
に先ほどの非表示スタイル.no-fouc
を追加している。
※参考:Advanced Features: カスタム `Document` | Next.js
「_app.tsx」でstyles.css
を読み込む
import '../styles/styles.css';
MyApp
で副作用フックuseEffect()
を設定
function MyApp({ Component, pageProps }) { // For FOUC useEffect(() => { const removeFouc = (foucElement) => { foucElement.className = foucElement.className.replace('no-fouc', 'fouc'); }; removeFouc(document.documentElement); }); // 中略 }
- 変数
removeFouc
はアロー関数で引数はfoucElement
foucElement
のclass名の.no-fouc
を.fouc
に置換 removeFouc()
を実行、引数はdocument.documentElement
中身は先ほどのFOUC対策の処理
※参考:next.js 🚀 - 追い風+ cssモジュールを使用した開発でちらつくスタイル | bleepcoder.com
documentElement
はルートタグすなわちhtmlタグのこと
※参考:Document.documentElement - Web API | MDN
これでDOMを読み終わったあとに.no-fouc
が.fouc
に置換されてページが表示される
最後に
これで全体的な設定は完成。今回得た知見
- ローカル環境でホットリロードが効かないためBabelの設定を変更する
- Next.jsの全体にわたる設定は「_app.tsx」で設定
- htmlタグ、bodyタグは「_document.tsx」で設定
- クラウド環境のFOUC対策としてhtmlタグを初期状態を非表示にし、DOM読み込み後に表示させる
特にFOUC対策は一番苦戦したので、解決できて良かったです♪
長くなったので今回はここまでにします。次は各ページの内容に入っていきます。
それではまた!
続き書きました!コンポーネント&モジュール編です♪
※参考:www.i-ryo.com
※参考:ReactでWebアプリを作るシリーズまとめ
qiita.com