🗒️

Next.jsでNotion APIを使った<ul>と<li>の取り扱い【notion-blog-nextjs】

2022/03/12に公開

やること

  • notion-blog-nextjs を元にして
  • <li><ul> で囲み
  • Notionと表記を(だいたい)同期させる
  • …あっ、CSSは tailwind で書いていきます

notion-blog-nextjsを導入する

  • Githubに公開されている samuelkraft / notion-blog-nextjs
  • 好きなフォルダに clone して
  • 該当ディレクトリに cd ~ して
  • npm install してパッケージをインストールして
  • npm run dev で動かせます
  • あとは .env.local にいろいろ書けば自分のNotionのデータを参照させられます。

詳しくは以下リポジトリのREADMEを参考にしてください。

https://github.com/samuelkraft/notion-blog-nextjs

notion-blog-nextjsのしくみ

  • ビルドすると index.js が組み立てられる
  • Notionのページはそれぞれ [id].js を雛形に組み立てられる

むっちゃ簡単に言うとこんな感じです。
つまり、 [id].js をいじればいろいろ変えられるということですね。

idページを編集する

[id].jsのここをいじると、idページの見た目を変えることができます。
自由に書き加えていきましょう。

export default function Post({ page, blocks }) {
  if (!page || !blocks) {
    return <div />;
  }
  return (
    <div>
      <Head>
        <title>{page.properties.Name.title[0].plain_text}</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <article className={styles.container}>
        <h1 className={styles.name}>
          <Text text={page.properties.Name.title} />
        </h1>
        <section>
          {blocks.map((block) => (
            <Fragment key={block.id}>{renderBlock(block)}</Fragment>
          ))}
          <Link href="/">
            <a className={styles.back}>← Go home</a>
          </Link>
        </section>
      </article>
    </div>
  );
}

今回大事になってくるのがここになります。

{blocks.map((block) => (
  <Fragment key={block.id}>{renderBlock(block)}</Fragment>
))}

今の時点ではとてもシンプルですが、ここに以下の機能を足していきます。

  • リストの状況を見ながら <ul>~</ul> を挿入する。
  • Notion側の表記に応じて、リストの点を数字かポチか決める。

つまるところ目標は

Notionで書いたこれの再現。

https://i.gyazo.com/746e64a0ebc8a5fedff8948947df94c1.png

とりあえず想定していることとして

  • 複数個のリストの取扱い
  • 種類の違うリストの取扱い
  • リストをつなげるとどうなるのか
  • リストをネストさせているとどうなるのか
  • ちゃんと他の要素(今回はプレーンなテキストも表示できるか)

を見ていきます。

とりあえず結論

こう書けばいい感じの挙動になります。(投げやり)
ちなみに、ここから tailwindを使って装飾しています。

export function make_ul_li(list_blocks, now_block = null) {
    const listclass = "ml-5 p-5";

    var ul_class;
    switch (list_blocks[0].type) {
        case "numbered_list_item":
            ul_class = "list-decimal";
            break;
        case "bulleted_list_item":
            ul_class = "list-disc";
            break;
    }
    return (
        <>
            <ul key={list_blocks.key} className={`${ul_class} ${listclass}`}>
                {list_blocks.map((list_block) => renderBlock(list_block))}
            </ul>
        </>
    );
}

export function Pages_template({ blocks, pagename }) {
    var disc_list = [];
    var decimal_list = [];

    const TYPE_DECIMAL = "numbered_list_item";
    const TYPE_DISC = "bulleted_list_item";
		const c = "flex flex-col w-5/6 m-auto content-center";
    var output_code = "";

    return (
        <div className={c}>
            <Header pagename={pagename} />
            <h1 className="text-4xl font-bold pb-5">{pagename}</h1>
            <section>
                {blocks.map((block) => {
                    if (~block.type.indexOf("list")) {
                        if (decimal_list.length > 0) {
                            switch (block.type) {
                                case TYPE_DISC:
                                    output_code = make_ul_li(decimal_list);
                                    disc_list.push(block);
                                    decimal_list = [];
                                    return output_code;
                                case TYPE_DECIMAL:
                                    decimal_list.push(block);
                                    break;
                            }
                        } else if (disc_list.length > 0) {
                            switch (block.type) {
                                case TYPE_DISC:
                                    disc_list.push(block);
                                    break;
                                case TYPE_DECIMAL:
                                    output_code = make_ul_li(disc_list);
                                    decimal_list.push(block);
                                    disc_list = [];
                                    return output_code;
                            }
                        } else if (
                            decimal_list.length == 0 ||
                            disc_list.length == 0
                        ) {
                            switch (block.type) {
                                case TYPE_DECIMAL:
                                    decimal_list.push(block);
                                    break;

                                case TYPE_DISC:
                                    disc_list.push(block);
                                    break;
                            }
                        }
                    } else {
                        if (decimal_list.length > 0) {
                            output_code = make_ul_li(decimal_list, block);
                            decimal_list = [];
                            return output_code;
                        } else if (disc_list.length > 0) {
                            output_code = make_ul_li(disc_list, block);
                            disc_list = [];
                            return output_code;
                        } else if (
                            decimal_list.length == 0 &&
                            disc_list.length == 0
                        ) {
                            return renderBlock(block);
                        }
                    }
                })}
            </section>
            <Footer />
        </div>
    );
}

これをいい感じに [id].js に流し込んでやると、

https://i.gyazo.com/5c1b60c98d7e24c19aa15f7018c67209.png

こんな感じになります。

さしすせそ の部分や、 おおかかの間あたりのスペースが残念な結果になりましたが、
まあこう書くほうが悪いだろってことで無視していきます。

コード解説

分けて解説していきます。

Pages_templateについて

  • いろいろな変数や定数を設定していきます。
  • disc_listdecimal_listで、点のタイプを仕分けていきます。
  • constの類はいちいち変えるのがめんどくさい箇所に設定しています。
var disc_list = [];
var decimal_list = [];

const TYPE_DECIMAL = "numbered_list_item";
const TYPE_DISC = "bulleted_list_item";
const c = "flex flex-col w-5/6 m-auto content-center";
var output_code = "";
  • <Header ~/><Footer ~/> は他コンポーネントからimportしています。
    ちなみに中身は文字通りページのヘッダーやフッター、head部分の処理を担っています。
return (
        <div className={c}>
            <Header pagename={pagename} />
            <h1 className="text-4xl font-bold pb-5">{pagename}</h1>
            <section>
               ~
            </section>
            <Footer />
        </div>
    );
}

今回一番のキモです。

{blocks.map((block) => {
    if (~block.type.indexOf("list")) {
        if (decimal_list.length > 0) {
            switch (block.type) {
                case TYPE_DISC:
                    output_code = make_ul_li(decimal_list);
                    disc_list.push(block);
                    decimal_list = [];
                    return output_code;
                case TYPE_DECIMAL:
                    decimal_list.push(block);
                    break;
            }
        } else if (disc_list.length > 0) {
            switch (block.type) {
                case TYPE_DISC:
                    disc_list.push(block);
                    break;
                case TYPE_DECIMAL:
                    output_code = make_ul_li(disc_list);
                    decimal_list.push(block);
                    disc_list = [];
                    return output_code;
            }
        } else if (
            decimal_list.length == 0 ||
            disc_list.length == 0
        ) {
            switch (block.type) {
                case TYPE_DECIMAL:
                    decimal_list.push(block);
                    break;

                case TYPE_DISC:
                    disc_list.push(block);
                    break;
            }
        }
    } else {
        if (decimal_list.length > 0) {
            output_code = make_ul_li(decimal_list, block);
            decimal_list = [];
            return output_code;
        } else if (disc_list.length > 0) {
            output_code = make_ul_li(disc_list, block);
            disc_list = [];
            return output_code;
        } else if (
            decimal_list.length == 0 &&
            disc_list.length == 0
        ) {
            return renderBlock(block);
        }
    }
})}

日本語にすると、処理はだいたい以下のようになっています。

  • block.type”list”という文字列が入っているか?
    • 入ってます、それは間違いなく<li>に該当する要素です
      • disc_list、もしくは decimal_list に値が入っていますか?
        • 入っています
          • 現在値が入るのは同じリストですか?
            • 同じリストです、現在値をそのリストに追加して最初に戻ります。
            • 違うリストです
              • リストを <ul>にまとめて、リストは初期化します。
                違うリストに現在値を追加して最初に戻ります。
        • 入っていません
          • 現在値が入るのは同じリストですか?
            • 同じリストです、現在値をそのリストに追加して最初に戻ります。
            • 違うリストです
              • リストを <ul>にまとめて書き出します。
                リストは初期化します。
                違うリストに現在値を追加して最初に戻ります。
    • 入っていません、それは別の要素になります。
      • disc_list、もしくは decimal_list に値が入っていますか?
        • 入っています
          • リストを <ul>にまとめて書き出します。
            リストは初期化します。
            要素をそのまま書き出します。
            最初に戻ります。
        • 入っていません
          • 要素をそのまま書き出します。
            最初に戻ります。

で、 <ul>を書き出す際には以下の処理に通していきます。

export function make_ul_li(list_blocks) {
    const listclass = "ml-5 p-5";

    var ul_class;
    switch (list_blocks[0].type) {
        case "numbered_list_item":
            ul_class = "list-decimal";
            break;
        case "bulleted_list_item":
            ul_class = "list-disc";
            break;
    }
    return (
        <>
            <ul key={list_blocks.key} className={`${ul_class} ${listclass}`}>
                {list_blocks.map((list_block) => renderBlock(list_block))}
            </ul>
        </>
    );
}

以上です。
割と力技みたいな感じになったので、もっとスマートな書き方があればご教示ください。

詰まったところ

リストの個数はちゃんと .lengthつけてあげる

動かない

if (配列 > 0) 

動く

if (配列.length > 0) 

.mapは案外自由が効く

関数と考え方が似ているなと思いました。
仕組みさえわかればこっちのものですね。

Discussion