Next.jsで作ったブログにStyleを適用していく

TL;DR

前回の記事で、markdown をうまくレンダリングできるようになったので、次は Style を適用していく。適用すべき対象は、最初の記事に書いたように、

  • material-ui
  • Prism.jsでのcode syntax
  • amp-mathmlによる数式
  • Github markdown css

である。AMP対応するには鬼門である。

Styleの適用

Prism.js && Github markdown css

prism.js公式サイトからcssをダウンロードしておく。github-markdown-cssからダウンロードする。github-markdown-cssは自動生成なので!importantとかが使われていてAMPに対応できないのでそのへんは除いてしまう。

Next.js 12になって、_document.jsでcssをロードすると怒られるようになってしまった。 仕方ないので、現状はcssをjsのstringとして保存しておいて、componentでimportし、sytled-jsxで表現している。

css.js

export const css = `
  css
`

上のようなファイルを作成しておき

component.jsx

import {css} from "css.js"

export default function Component() {
  return (
    <>
      <div>styled</div>
      <style jsx>{css}</style>
    </>
  )
}

componentで読み込んで、そのままsytled-jsxに突っ込むことで、一応対応できている。

2021/07/01改稿 Next.js v11

webpack5を使っているとasset modulesを使うことでraw-loaderの機能が実装できる。まずはnext.config.jsに設定を書く。フルAMPなので、cssをimportすることは想定していない。

next.config.js

module.exports = {
  webpack(config, options) {
    config.module.rules.push({
      test: /\.css/,
      resourceQuery: /raw/,
      type: 'asset/source'
    })
    return config
  },
}

これでcssファイルをraw-loaderのように読み込める。

_document.js

import React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheets } from "@material-ui/core/styles";

import theme from "@libs/theme";

// @ts-ignore
import css from "../styles/github_markdown.css?raw";
// @ts-ignore
import prismCss from "../styles/prism.css?raw";
// @ts-ignore
import globalCss from "../styles/global.css?raw";

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="ja">
        <Head>
          {/* PWA primary color */}
          <meta name="theme-color" content={theme.palette.primary.main} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
  const sheets = new ServerStyleSheets();
  const originalRenderPage = ctx.renderPage;

  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
    });

  const initialProps = await Document.getInitialProps(ctx);

  return {
    ...initialProps,
    // Styles fragment is rendered after the app and page rendering finish.
    styles: [
      ...React.Children.toArray(initialProps.styles),
      <style
        key="custom"
        dangerouslySetInnerHTML={{
          __html: `${globalCss}\n${css}\n${prismCss}`,
        }}
      />,
      sheets.getStyleElement(),
    ],
  };
};
2020/9/7 raw-loaderを使った実装

そのあと、raw-loaderを使ってcssを_app.tsxでimportして、直接埋め込む。できるならMarkdownのページだけで読み込みたいが...

ちょっとmaterial-ui成分も入ってしまっているが、_document.jsは以下の感じ。

_document.js

import React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheets } from "@material-ui/core/styles";

import theme from "@libs/theme";

// @ts-ignore
import css from "!!raw-loader!../styles/github_markdown.css";
// @ts-ignore
import prismCss from "!!raw-loader!../styles/prism.css";
// @ts-ignore
import globalCss from "!!raw-loader!../styles/global.css";

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="ja">
        <Head>
          {/* PWA primary color */}
          <meta name="theme-color" content={theme.palette.primary.main} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
  const sheets = new ServerStyleSheets();
  const originalRenderPage = ctx.renderPage;

  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
    });

  const initialProps = await Document.getInitialProps(ctx);

  return {
    ...initialProps,
    // Styles fragment is rendered after the app and page rendering finish.
    styles: [
      ...React.Children.toArray(initialProps.styles),
      <style
        key="custom"
        dangerouslySetInnerHTML={{
          __html: `${globalCss}\n${css}\n${prismCss}`,
        }}
      />,
      sheets.getStyleElement(),
    ],
  };
};

custom loaderでrefactorを使ってcodeをTokenに落とす作業をしておけばAMPでもコードがハイライトされる。順番の関係か、prismjsはダーク系のテーマにしたのに黒くならなかったので、github-markdown-css側で背景を黒にしておいた。

example

const MOD: usize = 1e9 as usize + 7;
>>> import pandas as pd
>>> pd.read_csv("/path/to/file.csv")

amp-mathml

KatexはAMPに対応できない。

そこで、まず、remark-mathを使って、mathinlineMathのノードに変換する。その後、custom loaderを使って、type === "math"type === "inlineMath"に対応するamp-mathmlを埋め込む。インラインの数式はparagraphのchildrenなので注意が必要。

example

インラインab\frac{a}{b}数式

普通の数式

k=1nNk=O(Nlogn)\sum_{k=1}^{n}{\frac{N}{k}} = O(N\log{n})

material-ui

2021/09/23

yarn add @mui/material @mui/styles @mui/lab @mui/icons-material @emotion/react @emotion/styled @emotion/server

Material-UIのversionが上がったので、色々設定が必要になった。Emotionベースなので、AMP対応する場合は注意が必要。@emotion/serverextractCriticalToChunksを使うのが重要らしい(参考)。

というのは、サーバーサイドレンダリングをnext.jsでするときに、CSSの読み込みがリセットされてしまうことがあるらしい(参考)。実際に自分の画面でも崩れていて、結構時間を溶かした。 幸いなことに、material-uiの公式がテンプレート例を作成してくれている(javascript, typescript)。これを参考にしながら_app.tsx_document.tsxを書き換えておく。あとnext.jsのリンクとmaterial-uiのリンクもclassNameの問題とかでうまく行かないことがあるので、Linkコンポーネントを作っておく。

!importantを使用しているコンポーネントは使用できないので注意が必要になる。

感想

スタイルの適用はこんな感じ。しかし、material-uiは結構がっつりcssっぽいものを触らないとだめで結構難しい。Bootstrapはだいたいよしなにやってくれていたので、css力が本当に無い。

この記事に関するIssueをGithubで作成する

Read Next