Next.js script を head 内に出力したい問題
Next.js v13 (page)
GTM (GA-4) のスクリプトを <head> 内に追加したい
GA-4 では <script>
を head タグのなるべく上部に、<noscript><iframe /></noscript>
を body タグの上部に置くようにと書かれている
next/script
を使った方法があるのでそれを参考にする
Next のドキュメントに cf. https://nextjs.org/docs/messages/next-script-for-ga
next/script
の <Script>
が出力されない問題
GTM 出力用のコンポーネント
// GTMScript.ts
import Script from 'next/script';
import { FC } from 'react';
export type GtmTrackingID = `GTM-${string}`;
type GTMScriptProps = {
gtmID: GtmTrackingID;
};
export const GTMScript: FC<GTMScriptProps> = ({ gtmID }) => {
return (
<Script id='googleTagManager'>
{`
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${gtmID}');
`}
</Script>
});
全ページ共通なので _document.tsx
/ _app.tsx
で試してみたが <script>
タグが消える
// /pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang='en'>
<Head>
<GTMScript gtmID={gtmID as GtmTrackingID} />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
=> <script>
タグは出力されない
// /pages/_app.tsx
import Head from 'next/head';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<GTMScript gtmID={gtmID as GtmTrackingID} />
</Head>
<Component {...pageProps} />
</>
);
}
=> <script>
タグは出力されない
<Head>
外にしてみる
_document.tsx
=> 🙅
// /pages/_document.tsx
export default function Document() {
return (
<Html lang='en'>
<Head />
<body>
<GTMScript gtmID={gtmID as GtmTrackingID} />
<Main />
<NextScript />
</body>
</Html>
);
}
=> <script>
タグは出力されない
_app.tsx
=> 🙆
// /pages/_app.tsx
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<GTMScript gtmID={gtmID as GtmTrackingID} />
<Component {...pageProps} />
</>
);
}
=> <body>
の最下部に出力される
<Script>
の strategy
を変更してみる
_app.tsx
では head 外に <Script>
を置くと <script>
タグが出力された
<script>
タグに data-nscript="afterInteractive"
という属性が付いていて、next/script の strategy
で設定できるっぽい
strategy
The loading strategy of the script. There are four different strategies that can be used:
beforeInteractive
: Load before any Next.js code and before any page hydration occurs.afterInteractive
: (default) Load early but after some hydration on the page occurs.lazyOnload
: Load during browser idle time.worker
: (experimental) Load in a web worker.
strategy="beforeInteractive"
だと <head>
内に script タグが出力される
// GTMScript.ts
export const GTMScript: FC<GTMScriptProps> = ({ gtmID }) => {
return (
- <Script id='googleTagManager'>
+ <Script id='googleTagManager' strategy='beforeInteractive'>
{`
/pages/_app.ts
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<GTMScript gtmID={gtmID as GtmTrackingID} />
<Component {...pageProps} />
</>
);
}
=> head 内に <script>
が出力される
<Head>
で追加した要素の後に追加される
※ ただし // /pages/_app.ts
import Head from 'next/head';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<meta name='next-head-meta' />
</Head>
<GTMScript gtmID={gtmID as GtmTrackingID} />
<Component {...pageProps} />
</>
);
}
👇 next/head
で追加した要素の後に strategy="beforeInteractive"
な <script>
が追加される
strategy="beforeInteractive"
でも _document.tsx
で読み込ませると出力されない
<Script id='googleTagManager' strategy='beforeInteractive'>
な script を出力する GTMScript
コンポーネントを _document.tsx
で import しても🙅
// /pages/_document.tsx
export default function Document() {
return (
<Html lang='en'>
<Head />
<body>
<GTMScript gtmID={gtmID as GtmTrackingID} />
<Main />
<NextScript />
</body>
</Html>
);
}
=> <script>
タグは出力されない
beforeInteractive
な script を _doument.js
で読み込ませるのは NG っぽい
ESLint の warning が出ていた
`next/script`'s `beforeInteractive` strategy should not be used outside of `pages/_document.js`. See: https://nextjs.org/docs/messages/no-before-interactive-script-outside-documenteslint@next/next/no-before-interactive-script-outside-document
You cannot use the
next/script
component with thebeforeInteractive
strategy outsidepages/_document.js
. That's becausebeforeInteractive
strategy only works insidepages/_document.js
and is designed to load scripts that are needed by the entire site (i.e. the script will load when any page in the application has been loaded server-side).
<Head>
内での使用するのは変わらず NG
// /pages/_app.tsx
import Head from 'next/head';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<GTMScript gtmID={gtmID as GtmTrackingID} />
</Head>
<Component {...pageProps} />
</>
);
}
=> strategy="beforeInteractive"
でも <Head>
内に配置すると <script>
タグは出力されない
next/script を使ったタグの出力
-
<Script>
を_document.tsx
内に配置してもscript
タグは表示されない -
<Script>
をnext/head
,next/document
問わず<Head>
内に配置するとscript
タグは出力されない -
_appt.tsx
の<Head>
外に置くとbody
の閉じタグ直前にscript
が出力される -
strategy="beforeInteractive"
オプションを与えた<Sctipt strategy="beforeInteractive">
なら<head>
タグ内に script が出力されるが、<Head>
コンポーネントで追加した要素の後に追加される
結論
strategy="beforeInteractive"
オプションを使い、 _app.tsx
で読み込ませると全てのページの <head>
内に script
タグを出力できる
// GTMScript.ts
import Script from 'next/script';
import { FC } from 'react';
export type GtmTrackingID = `GTM-${string}`;
type GTMScriptProps = {
gtmID: GtmTrackingID;
};
export const GTMScript: FC<GTMScriptProps> = ({ gtmID }) => {
return (
<Script id='googleTagManager' strategy='beforeInteractive'>
{`
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${gtmID}');
`}
</Script>
});
// /pages/_app.tsx
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<GTMScript gtmID={gtmID as GtmTrackingID} />
<Component {...pageProps} />
</>
);
}
※ ただし next/script
の <Script>
コンポーネントを使用した場合、html の <head>
の上部に script
タグを出力するのは難しそう
GA-4 の noscript
公式だと下記のような <noscript>
を <body>
タグ開始直後に置くように言われている
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
_document.tsx
に書いても問題ない
noscript は 通常のタグなので _document.tsx
に置いても問題なく出力される
GTMScript.ts
に <noscript>
を出力するコンポーネントを作成して読み込ませることにした
// GTMScript.ts
export type GtmTrackingID = `GTM-${string}`;
type GTMScriptProps = {
gtmID: GtmTrackingID;
};
export const GTMNonScript: FC<GTMScriptProps> = ({ gtmID = defaultGtmID }) => {
return (
<noscript>
<iframe
src={`https://www.googletagmanager.com/ns.html?id=${gtmID}`}
height='0'
width='0'
style={{ display: 'none', visibility: 'hidden' }}
/>
</noscript>
);
}
_document.tsx
で読み込ませれば OK
// /pages/_document.tsx
export default function Document() {
return (
<Html lang='en'>
<Head />
<body>
<GTMNonScript gtmID={gtmID as GtmTrackingID} />
<Main />
<NextScript />
</body>
</Html>
);
}
noscript の中がエスケープされている
<noscript>
の中が "
で囲まれた文字列として出力されている
<noscript>"<iframe src="https://www.googletagmanager.com/ns.html?id=XXXXX" height="0" width="0" style="display:none;visibility:hidden"></iframe>"</noscript>
All
noscript
content is escaped when javascript is enabled. Try this with a<strong>
tag and you'll see the same thing. However, when javascript is disabled, it works as expected.
<noscript>
タグは JavaScript が有効な時はエスケープされる仕様らしい
devtool で JS を切ってみるとエスケープされてない状態で出力された
devtool の メニュー から Run command
を選択 Run > Disabled JavaScript
を実行してリロード
JavaScript が off だと<noscript>
の中がタグとして有効化されていた
ただし、
Next.js の場合 static generate してても JS が動作しない環境だと厳しいので、この設定自体なくても良い気もする
script
タグを書けば Head で出力できる
直接 あまりお行儀は良く無さそう & 直接 script を実行させるリスクはありそうだが、<Script>
コンポーネントを用いなければ <Head>
内に直接 <script>
を書いて出力させることができた
// /pages/_app.tsx
import Head from 'next/head';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<script>{`
console.log("foo");
`}</script>
<Head>
<Component {...pageProps} />
</>
);
}
ただし、_app.tsx
より内部のページコンポーネントの <Head>
で指定した <title>
の方が先に出力されている
_document.tsx
の <Head>
(next/document
) はエスケープされ next/head
の後に出力される
// /pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang='en'>
<Head>
<script id='_document-head-script'>{`
const bar = "xxx";
console.log('bar');
`}</script>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
/pages/_document.tsx
の <Head>
内にも直接 script タグを書けば HTML の head 内に出力されたが、<script>
タグの内部のコードの値がエスケープされており、console も出力されないのでスクリプトが壊れているように見える
また _app.tsx
の <Head>
を使うより後に出力されるので、これなら先の <Script strategy='beforeInteractive'>
を使えば良いので積極的に使用するメリットは無さそう
<script>
の中身はエスケープされている?
直接記述した // /components/SiteHead.tsx
export const SiteHead: FC = () => {
return (
<Head>
<script id='site-head1'>{`console.log("site-head1")`}</script>
<script
id='site-head2'
dangerouslySetInnerHTML={{ __html: 'console.log("site-head2")' }}
></script>
<title>Next.app</title>
<meta name='site head component' />
</Head>
);
};
site-head1
は直接 JS を記述し、site-head2
は dangerouslySetInnerHTML
を使って JS そ挿入した
両方とも console.log も出力されて動作しているように見えるが、
Source で見てみると直接 JS を記述した site-head1
のコードはエスケープされており Uncaught SyntaxError: Unexpected token '&'
というエラーも発生している
Script タグの位置が移動している
Source 上は SiteHead
コンポーネントの通り <title>
の上に <script>
が表示されているが、dev tool で見ると script タグが SiteHead
コンポーネント の <Head>
の内容の最後に移動している
Next.js がレンダリング時に <script>
タグを改めて追加し直しているのかもしれないが、原因は良く分からなかった。
<script>
を返すものはうまく表示されない?
conponent で <script>
タグを返す JSX を使えば GA-4 のスクリプトを <head>
上部に出力できるのでは?と思ったが、コンポーネントが返した<script>
を dev tool 上で確認できず消えたように見える (Source 上は存在していて、console などは出力される)
// GTMScript.ts
export type GtmTrackingID = `GTM-${string}`;
type GTMScriptProps = {
gtmID?: GtmTrackingID;
};
export const GTMScript2: FC<GTMScriptProps> = ({ gtmID = defaultGtmID }) => {
return (
<script
id='gtm-script-2'
dangerouslySetInnerHTML={{ __html: `console.log('${gtmID}')` }}
></script>
);
};
dangerouslySetInnerHTML
を使った <script>
タグを返すコンポーネント
// SiteHead.tsx
export const SiteHead: FC = () => {
return (
<Head>
<GTMScript2 gtmID='GTM-xxx' />
<title>Next.app</title>
<meta name='site head component' />
</Head>
);
};
connected 前に GTM-xxx
が log に出力されているが、dev tool の <head>
内に script が見当たらない
Source 上だと SiteHead
に記述した位置に <script>
が出力されている
予想
<script>
タグを直接記述する方法は Next がレンダリング時に <secript>
タグを再配置しているのかもしれない
そう考えると Source 上に出力された状態で一度動作し、Next が再配置する際に消去されるとすれば説明がつく
<script>
タグを直接 JSX に記述する
通常の -
_document.tsx
の<Head>
(next/document
) に記述された<script>
はエスケープされて動作しない -
<Head>
(nex/head
) 無いに直接<script>
タグを記述した場合、そのまま<head>
内に出力される- 直接コードを記述する場合は
dangerouslySetInnerHTML
を使ったほうが良い -
dangerouslySetInnerHTML
を使わなくとも追加したコードは動作するが、エスケープされた<script>
が source 上に出力されエラーが出たりする
- 直接コードを記述する場合は
-
_app.tsx
の<Head>
より内部のページコンポーネントの<Head>
の内容のほうが先に出力される-
_document.tex
の<Head>
の内容は_app.tsx
のものより後になる
-
-
<script>
を返すコンポーネントを<Head>
内においた場合-
dangerouslySetInnerHTML
でコードを追加したスクリプトは動作しているように見えるが、dev tool で<script>
タグの存在を確認できない -
dangerouslySetInnerHTML
を使わない場合は、Source 上に<script>
タグは確認できるが動作せず ( console などが出力されない)、dev tool 上でも<script>
タグの存在を確認できない
-
結論
<script dangerouslySetInnerHTML={{__html:
JS のコード}}
/>は動作するが、挙動が怪しいので
<Script>` コンポーネントを使用したほうが安全だと感じた