Lume入門(第3回) - ページをタグ管理して検索性を高める

| 9 min read
Author: noboru-kudo noboru-kudoの画像

前回はLumeのテンプレートエンジンとしてJSX/MDXプラグインを使用する方法を見てきました。

サイト運営が順調でページが増えてくると、目的のページを探すのが辛くなってきます。
こんなときは、ページにタグ(目印)を付けて検索性を高める手法がよく使われますね。

今回は、Lumeを使ってタグ付けされた記事の一覧ページを生成する方法をご紹介します。
また、一覧ページにリストアップする記事が多い場合に有効なページネーション機能も見ていきます。

これらは、LumeのSearchプラグインとPaginateプラグインを使うことで簡単に実装できます。

両プラグインともに、Lume本体にプレインストールされているため、すぐに使い始められます。

Information

2023-12-08にLumeがv2にメジャーアップデートしました。これに伴い本記事もv2で動作するよう更新しました。

ページにタグをつける

#

まずはページ(記事)にタグを付けます。マークダウン(含むNunjucks/MDX)の場合はフロントマターにtags変数を追加し、配列形式でタグを複数指定します。

---
title: Lumeで始めるブログサイト運営 - その1
layout: layouts/blog.njk
date: 2023-01-01
tags: ["Lume", "SSG", "Deno"]
---

上記はタグとしてLumeSSGDenoと3つのタグをページに付けています。

また、一覧ページのソート条件で使用するためにdateも指定しています。このdate変数はLumeでは特殊な変数でページ作成日として扱われます。
詳細は以下公式ドキュメントを参照してください。

ここではblogsディレクトリ配下にこのマークダウンファイルを10ページ分作成します。

blogs
├── lume-1.md
├── lume-2.md
├── lume-3.md
├── lume-4.md
├── lume-5.md
├── lume-6.md
├── lume-7.md
├── lume-8.md
├── lume-9.md
└── lume-10.md

各ファイルのdate変数(作成日)は、1日ずつずらした日付(2023-01-01 ~ 2023-01-10)としました。
これらは/blogs/lume-{num}/[1]でアクセス可能なページとして生成されます。

JSXで作成したページにタグを付ける

JSXで作成したページにタグを付ける場合は、以下のようにtags変数をexportします。

export const title = "Lumeで始めるブログサイト運営 - その1";
export const layout = "layouts/blog.njk";
export const date = "2023-01-01";
export const tags = ["Lume", "SSG", "Deno"];

export default () => (<div>JSXでタグ付けする</div>);

一覧ページを作成する

#

事前準備が終わりましたので、タグ付けされた記事の一覧ページを作成します。
ここではLumeでタグ付けされたページを対象とします。

一覧ページの作成も通常ページの作成と基本的には変わりませんが、Lumeがビルトインで提供するSearchプラグインを使用します。

NunjucksバージョンとJSXバージョンで見てみます。

  • Nunjucksテンプレート
---
layout: layouts/blog.njk
url: /tags/lume/
title: Lumeのページ一覧
---

{%- for page in search.pages('Lume', 'date=desc') %}
<div>
  <a href="{{ page.url }}">{{ page.title }}</a>
</div>
{%- endfor %}
  • JSX(TSX)テンプレート
export const layout = "layouts/blog.njk";
export const url = "/tags/lume/";
export const title = "Lumeのページ一覧";

export default ({ search }: Lume.Data) => {
  return (
    <>
      {search.pages("Lume", "date=asc").map((page, index) => (
        <div key={index}>
          <a href={page.url}>{page.title}</a>
        </div>
      ))}
    </>
  );
};

Nunjucksの場合はグローバル変数、JSXの場合はPropsとしてsearchオブジェクトを受け取り、pagesメソッドから対象のページを取得しています。
pagesメソッドは第1引数に検索条件[2]、第2引数にソート順、第3引数にリミット件数を指定します。つまり、ここではLumeタグで作成日の降順という条件になります(リミットは指定なし)。

第1引数の検索条件はタグだけでなく、フロントマター変数は何でも指定可能です(タグ以外の場合は変数の指定も別途必要)。もちろん複数条件や否定や前方一致等にも対応しています。
詳細は以下公式ドキュメントやソースコードを参照してください。

どちらのテンプレートでも、生成されるページは以下のような見た目になります。

search example

Lumeタグがついているページが新しい順に一覧化できている様子が分かります。

全てのタグの一覧ページを生成する

#

先ほどは特定タグの一覧ページを作成しましたが、これだとタグが増える度に一覧ページを実装する必要があり効率的とは言えません。
事前に全タグを収集して、それぞれの一覧ページを生成する仕組みを作るのが理想的です。

このようなケースに対応して、Lumeではジェネレーター関数を利用して1つのテンプレートで複数のページを生成できます。

ジェネレーター関数はJavaScriptのものです。テンプレートエンジンとしてもJavaScript(こちらもビルトインです)を使います。

先ほどの同等のHTMLを出力するテンプレートは、以下のようになります(ここではTypeScriptを使用してます)。

export const layout = "layouts/blog.njk"; // 全ページ共通のフロントマター

export default function* ({ search }: Lume.Data) {
  const tags = search.values("tags"); // 全タグを収集
  for (const tag of tags) {
    const links = search.pages(tag as string, "date=desc").map((page) =>
      `<div><a href="${page.url}">${page.title}</a></div>`
    );
    yield {
      // ページ別のフロントマター
      title: `${tag}のページ一覧`,
      url: `/tags/${tag}/`,
      // ページコンテンツ
      content: links.join("")
    };
  }
}

先ほどより若干複雑ですが内容は自明です。

  1. searchオブジェクトのtagsメソッドでタグを収集
  2. それぞれについてpagesメソッドで対象ページを検索
  3. ページコンテンツ(HTML)を生成
  4. フロントマターとページコンテンツをyieldで返す

なお、JavaScriptをテンプレートとする場合は、それがページ生成用のテンプレートであることをLumeに示すため、ファイル名のサフィックスとして<file-name>.page.(js|ts)とする必要があります(デフォルト)。

先ほどLumeSSGDenoの3つのタグをページに指定していますので、これを実行すると以下3ページの一覧が生成されます(どのページも内容はほとんど同じです)。

  • /tags/Lume
  • /tags/SSG
  • /tags/Deno

もちろん、この実装であればタグが増えても追加実装は不要です。

ロジックとビューを分離する

ここではHTMLレンダリング部分もJavaScriptテンプレート内に記述しました。
ロジックとビューで実装を分離したい場合は、HTML部分はNunjucks等のJavaScript以外で記述し、これをレイアウトとして指定する方法があります。
本ケースでは以下のようになります。

  • Nunjucksテンプレート(post-list.njk)
---
layout: "layouts/blog.njk"
---
{%- for page in results %}
<div>
  <a href="{{ page.url }}">{{ page.title }}</a>
</div>
{%- endfor %}
  • JavaScriptテンプレート
// 全ページ共通のフロントマター
export const layout = "layouts/post-list.njk"; // 一覧ページ用のレイアウト

export default function* ({ search }: Lume.Data) {
  const tags = search.values("tags"); // 全タグを収集
  for (const tag of tags) {
    yield {
      // ページ別のフロントマター
      title: `${tag}のページ一覧`,
      url: `/tags/${tag}/`,
      // 検索結果を一覧ページ用のレイアウトに連携
      results: search.pages(tag as string, "date=desc")
    };
  }
}

Nunjucksで作成したレイアウトファイルを、JavaScriptテンプレート側でlayout変数に指定しています。
この例だとHTMLがシンプルすぎて効果を感じられませんが、複雑なテンプレートになる場合は分離した方がスッキリとします。

JSXを使って1テンプレートで複数ページを生成する

JSXテンプレートもJavaScriptですので、同様のことが可能です。
以下はJSX(TSX)テンプレートを使った場合のジェネレータ関数部分の抜粋です。

export default function* ({ search }: Lume.Data) {
  const tags = search.values("tags"); // 全タグを収集
  for (const tag of tags) {
    const links = search.pages(tag as string, "date=desc").map((page, index) =>
      <div key={index}><a href={page.url}>{page.title}</a></div>
    );
    yield {
      // ページ別のフロントマター
      title: `${tag}のページ一覧`,
      url: `/tags/${tag}/`,
      // ページコンテンツ
      content: links
    };
  }
}

実装としてもHTML部分がJSXに変わるだけなので、JSXプラグインを有効にしている場合はこちらを利用するのがお勧めです。
ビュー部品としてJSXのカスタムコンポーネントを使ったりするのも簡単です。

ページネーションを使って一覧ページを作成する

#

最後にページネーションを使ってみます。
タグ別に一覧ページを作ったとはいえ、汎用的なタグの場合は一覧に表示するページは大量になります。

そのようなケースではページネーションを使うことが多いかと思います。
このページネーションを手動で実装するのは結構面倒ですが、LumeにはPaginateプラグインがビルトインで提供されており、特別な設定なしで利用可能です。

先ほど10ページのサンプル記事を作成しています。ここでは1ページ3件としてページネーション付きの一覧ページを作成します。

export const layout = "layouts/blog.njk";
export default function* ({ search, paginate }: Lume.Data) {
  const tags = search.values("tags"); // 全タグを収集
  for (const tag of tags) {
    // paginateプラグインを使ってページネーションを実行
    const paginateResults = paginate(search.pages(tag as string, "date=desc"), {
      // 1ページあたり3件
      size: 3,
      // 1ページは/tags/<tagname>/、2ページ目以降は/tags/<tagname>/<n>/
      url: (n: number) => `/tags/${tag}/${n > 1 ? `${n.toString()}/` : ""}`, 
    });
    for (const paginateResult of paginateResults) {
      const links = paginateResult.results.map((page) =>
        `<div><a href="${page.url}">${page.title}</a></div>`
      );
      yield {
        title: `${tag}のページ一覧`,
        url: paginateResult.url,
        // ページコンテンツ
        content: `
<div>${paginateResult.pagination.page} / ${paginateResult.pagination.totalPages}</div>
${links.join("")}
${paginateResult.pagination.previous ? `<a href="${paginateResult.pagination.previous}">前ページ</a>` : "前ページ"}
<span>|</span>
${paginateResult.pagination.next ? `<a href="${paginateResult.pagination.next}">次ページ</a>` : "次ページ"}`,
      };
    }
  }
}

ここでのポイントはpaginateを呼んでいる部分です。第1引数でSearchプラグインの結果、第2引数にページネーション設定(size/url)を渡します。
Paginateプラグインはこの条件に従って、Searchプラグインの検索結果を分割してくれます。
Paginateプラグインではresultsに分割結果、paginationに現在ページや次/前ページURL等のページネーションに必要な情報を格納してくれます。

後はその結果に従ってページコンテンツを生成するだけです。とても簡単ですね。

上記は各タグ別に以下のページを生成します。

  • 1ページ目: /tags/<タグ名>/
  • 2ページ目: /tags/<タグ名>/2/
  • 3ページ目: /tags/<タグ名>/3/
  • 4ページ目: /tags/<タグ名>/4/

以下実際に生成される一覧ページの1つです。

lume pagination plugin

まとめ

#

今回はLumeでページのタグ管理を実践しました。
SearchプラグインとPaginateプラグインを使うことで、簡単かつ柔軟に実装できることが分かります。
これらのプラグインはタグ管理に限らず、アイデア次第で様々な用途で使用できるものです。
ある程度ページ数が多い場合は、このプラグインに使い慣れておくとサイト管理が楽になってくると思います。


  1. フロントマターとしてurlの指定をしていない場合は、デフォルトでパスがURLとなります。 ↩︎

  2. 検索条件を指定しない場合は全てのページが返却されます。 ↩︎

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。