BlogにTags機能をつける工程

2023/06/04 7 minute read

続・Gatsby+Contentful Tags|シラフになって考えたら楽しかった

BlogにContentfulのTags機能を追加した (2023/06/04 : Gatsby+Contentful Tags)の続きです。

もくじ

とりあえず、インターネット検索で見かけたTagsページの作り方を、Codeのコピペでは埒が開かないと気づいたときに、ひとまずシラフになって設計図とはいえないポンチ図を書き出しました。

制作の鍵はPenです。紙とペン。

  • まずは気ままにPostたちにつけられたTagを、クリックした先に表示する /tags/$display_tag/ というページ群が、Tagの数だけ必要。
    定数のない動的につくるページだが、外観は「リストである」ためBolgトップと同レイアウトで。

🩷成果物:Mac mini というタグ名を押下した例:/tags/macMini/

  • リストから開く、各Postページはすでにあるので不要だが、PostページのTag群は、それぞれ該当の /tags/$display_tag/ へリンクさせる。displayに見せる表示
    このStarterの場合は、Tags表示はされていたのをContentful Tagsに差し替えてリンクもつけるという工程。

🩷成果物:4つのタグがPostの末尾に表示されている例:/blog/gatsby-cloud/

  • タグ一覧表。WordPressウィジェットで「タグ・クラウド」と呼ばれているもの。
    (うちのBlogに要るかぁ?)とあまり必要を感じなかったが「一望できる・網羅された」はユーザー体験としては大切と考えるタチなので、 /tags/$display_tag/ 各ページのフッター近くにTAG Cloudとして載せた。
    TAG Cloudを書くために作業用でつくった1枚もの⭐️ALL TAGSもTAG Cloudの右隅にひっそり鎮座。
    大量に多方面記事を載せるBlogなら重宝するもの。

  • 因みにBlogトップでは以前と同じくTagを表示するのみでLinkをしないため、Linkのあるなしでブロックの高さに変化をつけている。


createPage

gatsby-node.js
tags.forEach((tag) => {
    createPage({
      path: `/tags/${tag.contentful_id}/`,
      component: tagIndex,
      context: {
        slug: tag.contentful_id,
        name: tag.name,
      },
    })
  })

Tagページの動的な生成は、すでにあるBlogPost用のcreatePageと同じ要領でサクッと書ける。
Postの場合は、if (posts.length > 0) { }   (0でなければ)条件下にあるが、IFは書かなかった乱暴者。

forEachで置き換え元となるtags. は result.のContentful Tag(33行目)

gatsby-node.js
const { createPage } = actions

const blogPost = path.resolve('./src/templates/blog-post.js')
const tagIndex = path.resolve('./src/templates/tags-index.js')

const result = await graphql(
    `
      {
        allContentfulBlogPost {
          nodes {
            title
            slug
            metadata {
              tags {
                contentful_id
                name
              }
            }
          }
        }
        allContentfulTag {
          nodes {
            contentful_id
            name
          }
        }
      }
    `
  )
// ......
// ▼ ▼ ▼ resultで得られた allContentfulTag.nodes を代入

const tags = result.data.allContentfulTag.nodes

component: tagIndex, はテンプレートファイルを指すパスを直接書くケースを多く見たが、このStarterの場合は、3行目のように先に代入しておく手法だったのでTgasも4行目 path.resolve() で同様にテンプレートを渡している。


余談:ケバブケースじゃなくていい

Gatsbyのタグ、ContentfulTaulのTags、といったキーワードから来た人なら、ニッチでないメジャーなMarkdownのfrontmatterでの手法はとっくに目にしていると思う。

例でいうと製品名:Mac mini をタグ付けしたとき
Display表示はスペースありでも、slugとしてはよろしくないためハイフンを挟んだ kebab-case に置き換える手法。 frontmatterのTagsは、Tags[0],Tags[1],Tags[2],・・・と一次元配列なので、取り出し方は単純だが、実態とslugを置き換える必要が生じる場合あり。

漢字が入ると急にむずかしく感じるが、要は「空白スペース」を見つけたら「ハイフン」に置き換えてslugとする。
他方、Contentful Tags は最初から名前とIDを持ってつくられているため

実態:自分がつけたタグそのもので、Displayに表示する名 = tag.name と
slug:にすべきID = tag.contentful_id をそのまま使い分けられる。

createPage の context: として渡しているのはCodeに載せたとおり。

context: として渡されるんだから、Postページでの扱いはお茶の子さいさいだろう!と思いきや、これがなかなか慣れない初心者には難関になった。


context:の受け渡し

書籍もチェートリアルも読まずに他人のソースだけ見て、どうにかしようという魂胆がまず遠回りの要因なんすが!
パターンとして多かったのが、こういうdataをマルッと渡すもの

const posts = get(this, 'props.data.allContentfulBlogPost.nodes')

渡す側は

gatsby-node.js
context: {
	slug: post.slug,
	previousPostSlug,
	nextPostSlug,
	article: post,
}

slugと article: post (マルッとdata) と next / previous など前後ページの繋がりをつくったものを渡すケースが当然ながら情報として多く

contextで渡されたものは $context_name : $slug などで Queryのフィルターに使える。という理解までは localhost:8000/___graphql でじっくりGraphQLを見ると気づくのだが

const tagname = get(this, 'props.pageContext')

ズバリのたったこれだけが、導き出されるまでに時間を喰った。

結局・・・本家がわかりやすかった。
https://www.gatsbyjs.com/docs/creating-and-modifying-pages/

ドキュメントを翻訳も交えて真剣に眺めた結果、直接的なコピペネタはなくても、props なんだよな、 pageContext はキャメルケースだな、とか「目に伝えてくる」
今読み返すと下から2番目のcodeSnippetで気づいたのか。

On your pages and templates, you can access your context via the prop pageContext like this:
(ページとテンプレートでは、次のように prop pageContext を介してコンテキストにアクセスできます)

import React from "react"

const Page = ({ pageContext }) => {
  return <div>{pageContext.house}</div>
}

export default Page

手法が少し違うだけな代入ケースが見えたら自分のStarterに合わせた3行目を加筆し

const posts = get(this, 'props.data.allContentfulBlogPost.nodes')
const tags = get(this, 'props.data.allContentfulTag.nodes')
const tagname = get(this, 'props.pageContext')
	return (
		........
	)	

createPageでつくられる /tags/$display_tag/ のテンプレートで

/src/templates/tags-index.js
<h1 className={styles.title}>TAGS : {tagname.name}</h1>

ようやく対象のTag名をページのタイトルとして埋められました。

たったこれだけだが、Tagリンクを表示するよりずっと難関だった件。


TagIndexQueryのソース

/src/templates/tags-index.js
export const pageQuery = graphql`
query TagIndexQuery ($slug: String!){
	allContentfulTag {
		nodes {
			contentful_id
			name
		}
	}
	allContentfulBlogPost(
		sort: { publishDate: DESC }
		filter: {metadata: {tags: {elemMatch: {contentful_id: {eq: $slug }}}}}
	){
		nodes {
			title
			slug
			publishDate(formatString: "YYYY/MM/DD")
			metadata {
				tags {
					contentful_id
					name
				}
			}
			heroImage {
				gatsbyImage(
					layout: FULL_WIDTH
					placeholder: BLURRED
					width: 424
					height: 212
				)
			}
			description {
			raw
			}
		}
	}
}
`

まずTagsリンクから対象となるPostを絞り込むフィルターに、gatsby-node.jsのcreatePageから context: として渡された slug: tag.contentful_id, を11行目で使っています。{eq: $slug }

filter: {metadata: {tags: {elemMatch: {contentful_id: {eq: $slug }}}}}

そのPostが持つTagsの中に $slug と同じ文字列の contentful_id があるかどうか。 elemMatch: で確認。

事前に $slug を文字列化する処理が2行目 ($slug: String!)
allContentfulTag { }
allContentfulBlogPost( ) { }

TagとPost 両方のdataを GraphQLに要求するpageQuery


PostページにTags <Link to={ $slug }>をリンクを表示する

/src/templates/blog-post.js
<small className={tagstyles.tags}>
{post.metadata.tags.map(tag => (
	<div key={tag} className={tagstyles.tag}>
		<Link to={`/tags/${tag.contentful_id}`}>{tag.name}</Link>
	</div>
	))}
</small>

styleやHTMLタグもそのまま転記してますが、2行目から6行目のマッピングで置き換え。

日本語で(写像とは)とググると、めちゃくちゃ的確な説明が出てきました。

集合の各元(げん)を他の集合(または同じ集合)の元にそれぞれ対応させること。
「実数の対から虚数への―」
map(ping) の訳。同一集合内で行うのは特に「変換」と言う。

上のソースは、Postページや /tags/${tag.contentful_id} 自身にも「TAG Cloud」として載せています。
Bolgトップだけは、Tag表示のみリンクなしの使い方をしていますが、Gatsby developの開発環境では問題なかったものが、Buildエラーになりました。  

path/ に問題がある
post.metadata.tags. は未定義だ

といったエラー内容で思い当たることが一つ。
Contentful製のこのStarterは、Webで見る見本ソースより小洒落ているというか、スマートというか・・・だらだらと1ページに書かずに適度にcomponents化してあるのも、お手本になるなぁと気に入っていますが、components化すると階層は深くなるんですね。

その一例が問題になったBolgトップ
src/pages/blog.js 本体にはごく短くHero-Imageと「BLOG」というページタイトルまで。
Postを並べているGridは

/src/pages/blog.js
<ArticlePreview posts={posts} />

とcomponentsに渡して任せている。
見当はつくが対処法はまったく思いつかん。というときに救いの記事💜

⭐️ gatsby build 時の「WebpackError: TypeError: Cannot read property 'hoge' of undefined」対処法

対処法:エラーとなっているプロパティ(この場合はhoge)の前に?.を付け、?.hogeとすることで解消

/src/components/ArticlePreview.js
<ArticlePreview posts={posts} />
// で{posts}を渡された先のcomponentsで、metadata?. とオプショナルチェイング演算子を挿入

<small className={tagstyles.tags}>
	{post.metadata?.tags.map(tag => {
	return (
		<div key={tag.contentful_id} className={tagstyles.tag}>
		{tag.name}
		</div>
	)
	})}
</small>

説明が的確だったのでそのまま引用します。

?.とは何なのか

調べてみると、オプショナルチェイング演算子と呼ばれるものみたいです。

MDNによると、深い入れ子構造になったサブプロパティにアクセスする際は、各プロパティ間の参照を確認する必要があるとのこと。今回のコードで言うと、data.allFile.nodes内にfindでヒットした要素が存在することを確認した上で、publicURLを取得する必要があるようです。

これを暗黙的にやってくれるのが、オプショナルチェイング演算子です。