Next.jsでhtmlタグのprefix属性をページごとに出し分ける

@Panda_Program

Custom DocumentでNext.jsで各ページのHTMLタグを上書きする

Next.js には Custom Document という機能があります。これは、各ページで共通の HTML を上書きするための機能です。

html タグ、body タグに属性を付与したり、SEO 対策をするために meta タグを記述することに使われます。

今回は、OGP の公式ドキュメントに記載されている prefix 属性と値(prefix="og: http://ogp.me/ns#")を html タグに付与する方法を紹介します。

Next.jsでhtmlタグにprefixを付与する

pagesディレクトリに _document.tsx を作成します。以下のように記述すると、全てのページの html タグに prefix 属性が付与されます。

_document.tsx
import Document, { Head, Html, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  render() {
    return (
      <Html prefix="og: http://ogp.me/ns#">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

404ページはprefixを付与しないための設定をする

Next.js では pagesディレクトリに 404.tsx を作成すると、リソースが存在しないときに自動で 404 ページが表示されます。

この 404 ページなど、特定のページでは html タグから prefix 属性を削除したいケースがあると思います。

その時は、getInitialProps内でパスの判定をして場合分けをしましょう。

_document.tsx
import Document, { Html, Head, Main, NextScript } from 'next/document'

const htmlPrefix = 'og: http://ogp.me/ns#'

class MyDocument extends Document<{ prefix: string | null }> {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    // 404 ページには prefix を設定しない
    const prefix = ctx.pathname.startsWith('/404') ? null : htmlPrefix

    return { ...initialProps, prefix }
  }

  render() {
    const prefix = this.props.prefix

    return (
      <Html prefix={prefix}>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument

getInitialPropsの return 前にconsole.log(ctx.pathname, prefix)を差し込み、$ npm run build を実行してみます。すると、以下のようなログが出力されます。

$ npm run build
> next-starter-kit@1.0.0 build /sample_program/react/nextjs-playground
> next build

info  - Creating an optimized production build
info  - Compiled successfully
info  - Collecting page data
/ og: http://ogp.me/ns#
/404
/recoil og: http://ogp.me/ns#
/redux og: http://ogp.me/ns#
info  - Generating static pages (32/32)
info  - Finalizing page optimization

Page                                                           Size     First Load JS
┌ ○ /                                                          2.9 kB         81.9 kB
├   /_app                                                      0 B              79 kB
├ ○ /404                                                       279 B          79.3 kB
├ ○ /recoil                                                    15.5 kB        94.6 kB
├ ○ /redux                                                     626 B          79.7 kB
+ First Load JS shared by all                                  79 kB
  ├ chunks/1e4bfcbb482c0ad992d70a6f3b42612b47faafd5.172fb8.js  5.19 kB
  ├ chunks/e2988e983c224d75e4d495ad6342b845f7d73227.4953e9.js  13.8 kB
  ├ chunks/framework.4d50c5.js                                 42.1 kB
  ├ chunks/main.5f9b00.js                                      6.77 kB
  ├ chunks/pages/_app.858252.js                                10.4 kB
  ├ chunks/webpack.50bee0.js                                   751 B
  └ css/fda66c1c8420c26251ed.css                               3.16 kB

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
   (ISR)     incremental static regeneration (uses revalidate in getStaticProps)

ログを見ると、/404 では空文字であることがわかりますね。

next export で出力を確認する

$ next build && next exportを実行し、 HTML を出力して結果を確認してみましょう。

index.html404.htmlのを比較します。

out/index.html
<!DOCTYPE html>
<html prefix="og: http://ogp.me/ns#">
<head>
<!--  ...  -->
out/404.html
<!DOCTYPE html>
<html>
<head>
<!--  ...  -->

これで prefix 属性の出し分けができました!

ページ遷移時にhtmlの変更に対処する

なお、上記のままでは 404 ページから通常のページに遷移したとき、prefix 属性が存在しないままになります。反対に、通常ページから 404 ページにリンクしている場合、404 ページでも prefix 属性が付与されたままになります。

これは、そもそもクライアントサイドルーティングでは Next.js が ページごとに JS で表示するコンテンツを書き換えているのですが、その際 html タグまでは書き換えないからです。

OGP は特定の Web ページを SNS 等でリッチに表示するための手段なので、ページ遷移時の挙動まで考慮しなくても良いかもしれません。

ただ、もしページ遷移時も prefix を出し分けしたいのであれば、以下のような Custom Hooks を作って対応可能です。

htmlにprefix属性を付与するCustom Hooks

prefix 属性を付与したり削除するためには、タグの属性を操作するsetAttributeremoveAttributehasAttributeという API を活用します。

まず、prefix 属性を付与するuseAddHtmlPrefix を作成します。

useAddHtmlPrefix.ts
import { useRouter } from 'next/router'
import { useEffect } from 'react'

const prefix = 'prefix'
const htmlPrefix = 'og: http://ogp.me/ns#'

export default function useAddHtmlPrefix() {
  const { pathname } = useRouter()
  const is404Page = pathname.startsWith('/404')

  useEffect(() => {
    if (is404Page) {
      return
    }

    const hasHtmlPrefix = document.documentElement.hasAttribute(prefix)

    // prefix が存在しない場合、 404 以外のページに prefix を設定する
    if (!hasHtmlPrefix) {
      document.documentElement.setAttribute(prefix, htmlPrefix)
    }
  }, [is404Page])
}

この Custom Hooks を_app.tsxで呼び出します。

pages/_app.tsx
import { AppProps } from 'next/app'
import useAddHtmlPrefix from '~/hooks/useAddHtmlPrefix'

const App = ({ Component, pageProps }: AppProps) => {
  useAddHtmlPrefix()

  return <Component {...pageProps} />
}

export default App

htmlのprefix属性を削除するCustom Hooks

useRemoveHtmlPrefix.ts
import { useEffect } from 'react'

const prefix = 'prefix'

export default function useRemoveHtmlPrefix() {
  useEffect(() => {
    const hasHtmlPrefix = document.documentElement.hasAttribute(prefix)

    if (hasHtmlPrefix) {
      document.documentElement.removeAttribute(prefix)
    }
  }, [])
}

この Custom Hooks を404.tsxで呼び出すと、html から prefix 属性を削除できます。

pages/404.tsx
import useRemoveHtmlPrefix from '~/hooks/useRemoveHtmlPrefix'

export default function Error() {
  useRemoveHtmlPrefix()

  return <div>404</div>
}

以上、Next.js で html の prefix 属性の値prefix="og: http://ogp.me/ns#"をページごとに出し分ける方法でした。

参考

Add lang attribute to html for better accessibility #9160

Happy Coding 🎉

パンダのイラスト
パンダ

記事が面白いと思ったらツイートやはてブをお願いします!皆さんの感想が執筆のモチベーションになります。最後まで読んでくれてありがとう。

  • Share on Hatena
  • Share on Twitter
  • Share on Line
  • Copy to clipboard