03. Gatsby.js&MDXのサイトにリンクカードの実装

Posted on May 3rd, 2021Updated on May 3rd, 2021
03. Gatsby.js&MDXのサイトにリンクカードの実装

どんな記事

Investment Tech Hack

投資歴8年目の兼業トレーダーが運営するサイト。資金管理やリスク管理、複利運用、破産の確率の解説。Pythonで行うバックテストの紹介や、プログラムの公開、自動売買作成の記録など。

investment.abbamboo.com

Investment Tech Hack

Gatsby.jsで作っているこのブログに、上記のようなリンクカードを作成する機能を実装しました。

  • 同一サイトの投稿ならslug
  • 外部サイトならURL

を、指定すると必要な情報を取得してリンクカードを生成します。

参考にしたサイト

元ネタというか、ロジックは以下を参考にしました。

GatsbyJSで作っているブログでリッチなリンクを貼れるようにした

モチベーション アプリやサービスを紹介する際にURLだけだと味気がないので、OGPの情報を拾ってきて、リンク先のタイトルや画像が表示されるようにしたかった noteとかでよく見るやつ できあがったもの こんな感じのやつ。アプリとかサービスの紹介とかで使っていきたい。 検討したサービス まさしく、今回の用途ならIfra…

kikunantoka.com

GatsbyJSで作っているブログでリッチなリンクを貼れるようにした

コーディングにあたっては以下の記事を参考に。

Gatsbyにおける外部取得画像へのgatsby-image適用方法 | Takumon Blog

なにこれGatsbyで画像を扱う場合gatsby-imageを使うとイイ感じに最適化してくれます。Webサイトのリポジトリ内の画像についてはgatsby-config.jsで簡単に設定できますが、今回はビルド時に動的に取得する外部画像に対し...

takumon.com

Gatsbyにおける外部取得画像へのgatsby-image適用方法 | Takumon Blog

外部イメージにgatsby-imageを適用する方法

www.deg84.com

外部イメージにgatsby-imageを適用する方法

GatsbyJSの外部リンクをリッチにする

www.deg84.com

GatsbyJSの外部リンクをリッチにする

実装にあたって確認したドキュメントは以下です。

Sourcing Content from JSON or YAML

As you work with Gatsby, you might want to source data from a JSON or YAML file directly into a page or component. This guide will cover…

www.gatsbyjs.com

Sourcing Content from JSON or YAML

Gatsby Node APIs

Documentation on Node APIs used in Gatsby build process for common uses like creating pages

www.gatsbyjs.com

Gatsby Node APIs

RichLinkの機能

同一サイト内のリンク。

10年間変わらない強み 書籍:Strength Finder

書籍「さあ、才能(自分)に目覚めよう Strength Finder 2.0」のまとめと書評です。強みの診断結果をもとに導き出した筆者自身の強みと弱点を掲載しています。筆者本人が、後で内容をさっと確認する備忘録を兼ねています。

books/strength-finder

10年間変わらない強み 書籍:Strength Finder
MD
<RichLink slug="books/strength-finder" title="test" />

外部サイトは、あらかじめlinks.yamlにURLを登録しておく必要がある。 登録されているURLのOGPをもとに生成する。

Investment Tech Hack

投資歴8年目の兼業トレーダーが運営するサイト。資金管理やリスク管理、複利運用、破産の確率の解説。Pythonで行うバックテストの紹介や、プログラムの公開、自動売買作成の記録など。

investment.abbamboo.com

Investment Tech Hack
MD
<RichLink href="https://investment.abbamboo.com/" />

独自のテキストを指定することもできる。

test

test

test

test
MD
<RichLink
  href="https://investment.abbamboo.com/"
  title="test"
  description="test"
  domain="test"
/>

ファイルの構成

  • links.yaml
  • gatsby-node.js
  • rich-link.js

links.yaml

  • 外部サイトのURLを登録する
  • URLをもとに取得したOGP情報をキャッシュしておく

gatsby-node.js

  • links.yamlに登録されているURLをもとにOGP情報を取得し、yamlファイルを更新する
  • すでに情報を取得したものにisFetched: trueを付与しておく(簡単なキャッシュ機構)
  • Gatsby.jsのcreateRemoteFileNode()で画像をDLしつつNodeを作成する
    • 扱う画像のURLはユニークである必要があるので、重複をチェックして連番のパラメータを付与してエラーを回避する
  • 作成したNodeに必要な情報を追加する

rich-link.js

  • リンクカードを作成するコンポーネント
  • links.yamlに登録されてるURLをhrefで指定すると外部リンクのリンクカードを生成する
  • サイト内のMDXで作成している記事をslugで指定すると内部記事のリンクカードを生成

コード

実装環境は以下の通り。

"axios": "^0.21.1",
"cheerio": "^1.0.0-rc.6",
"gatsby": "^3.2.1",
"gatsby-plugin-image": "^1.3.0",
"react": "^17.0.2",
"tailwindcss": "^2.1.1"

gatsby-node.js

gatsby-node.js
const fs = require("fs")
const url = require("url")
const yaml = require("js-yaml")
const axios = require('axios');
const cheerio = require('cheerio');
const { createRemoteFileNode } = require(`gatsby-source-filesystem`);

// sourceNodesにて外部画像のファイルノードを作成する
exports.sourceNodes = async ({ actions, createNodeId, cache }) => {
  // WEBサイトURLからHTMLを取得してOGPを抽出
  let yamlLinks = yaml.load( fs.readFileSync( './src/data/links.yaml', 'utf-8' ) )
  let writeTrigger = false
  let counter = {}
  for ( let i = 0; i < yamlLinks.length; i++ ) {
    if(
      !yamlLinks[i].isFetched &&
      'url' in yamlLinks[i]
    ) {
      const res = await axios.get( yamlLinks[i].url )
      const $ = cheerio.load( res.data )
      yamlLinks[i].title =
        $("meta[property='og:title']").attr('content') ||
        $('title').text()
      yamlLinks[i].description =
        $("meta[property='og:description']").attr('content') ||
        $("meta[name='description']").attr('content')
      yamlLinks[i].image_url =
        $("meta[property='og:image']").attr('content') ||
        $("meta[name='image']").attr('content')
      yamlLinks[i].domain = url.parse( yamlLinks[i].url ).hostname
      yamlLinks[i].isFetched = true
      writeTrigger = true
    }
    // 重複を確認
    let elm = yamlLinks[i].image_url
    counter[elm] = ( counter[elm] || 0 ) + 1;
    if ( counter[elm] > 1 ) {
      yamlLinks[i].counter = counter[elm]
      writeTrigger = true
    }
  }
  if ( writeTrigger ) {
    try {
      fs.writeFileSync( './src/data/links.yaml', yaml.dump(yamlLinks), 'utf8' )
    } catch (e) {
      console.error( e.message )
    }
  }

  // 取得したOGP画像URLをもとにファイルノードを生成
  await Promise.all( yamlLinks.map( async yamlLink => {
    const fileNode = await createRemoteFileNode({
      url: `${yamlLink.image_url}?${yamlLink.counter}`,
      cache,
      createNode: actions.createNode,
      createNodeId: createNodeId,
    });
    await actions.createNodeField({ node: fileNode, name: 'ogpImage', value: true, })
    await actions.createNodeField({ node: fileNode, name: 'url', value: yamlLink.url, })
    await actions.createNodeField({ node: fileNode, name: 'title', value: yamlLink.title, })
    await actions.createNodeField({ node: fileNode, name: 'description', value: yamlLink.description, })
    await actions.createNodeField({ node: fileNode, name: 'imageUrl', value: yamlLink.image_url, })
    await actions.createNodeField({ node: fileNode, name: 'domain', value: yamlLink.domain, })
  }));
}

rich-link.js

rich-link.js
import React from "react"
import { useStaticQuery, graphql, Link } from "gatsby"
import { GatsbyImage, getImage } from "gatsby-plugin-image"



const WrapCard = ({ isLink, linkTo, className, children }) => (<>{
  isLink ?
    <Link className={className} to={linkTo}>{children}</Link> :
    <a href={linkTo} className={className} target="_brank">{children}</a>
}</>)

const RichLinkCard = ({ isLink, linkTo, className, title, description, domain, imageSharp }) => (
  <WrapCard isLink={isLink} className={`flex justify-center items-center overflow-hidden my-4 mx-0 p-0 border rounded ${className}`} linkTo={linkTo}>
    <div className="flex-auto p-4">
      <p className="mb-2 font-semibold max-2-lines">{title}</p>
      <p className="mb-1 text-xs max-2-lines">{description}</p>
      <p className="text-xs max-1-lines">{domain}</p>
    </div>
    {
      imageSharp &&
      <GatsbyImage className="flex-none border-l" image={ getImage( imageSharp ) } alt={title} />
    }
  </WrapCard>
)

const RichLink = ({ className, slug, href, title, description, domain, }) => {
  const { posts, yamlLinks } = useStaticQuery(graphql`
    query {
      posts: allMdx(filter: {frontmatter: {draft: {eq: false}}}) {
        edges {
          node {
            slug
            frontmatter {
              title
              description
              eyecatch {
                childImageSharp {
                  gatsbyImageData(
                    placeholder: DOMINANT_COLOR
                    aspectRatio: 1
                    height: 144
                  )
                }
              }
            }
          }
        }
      }
      yamlLinks: allFile(filter: {fields: {ogpImage: {eq: true}}}) {
        edges {
          node {
            fields {
              title
              description
              domain
              url
            }
            childImageSharp {
              gatsbyImageData(
                placeholder: DOMINANT_COLOR
                width: 144
                height: 144
              )
            }
          }
        }
      }
    }
  `)

  const isLink = slug ? true : false
  const linkTo = isLink ? `/${slug}/` : href
  const targetEdge = isLink ?
    posts.edges.find(edge => edge.node.slug === slug ) :
    yamlLinks.edges.find(edge => edge.node.fields.url === href )

  if ( targetEdge ) {
    const title_ = isLink ?
      targetEdge.node.frontmatter.title :
      targetEdge.node.fields.title
    const description_ = isLink ?
      targetEdge.node.frontmatter.description :
      targetEdge.node.fields.description
    const domain_ = isLink ? slug : targetEdge.node.fields.domain
    const imageSharp = isLink ?
      targetEdge.node.frontmatter.eyecatch.childImageSharp :
      targetEdge.node.childImageSharp

    return <RichLinkCard
      isLink={isLink}
      linkTo={linkTo}
      className={ className || '' }
      title={ title || title_ }
      description={ description || description_ }
      domain={ domain || domain_ }
      imageSharp={imageSharp}
    />

  } else {
    return <a href={linkTo} className={className} target="_brank">{linkTo}</a>
  }
}

export default RichLink
同じタグの記事
著者

タカハシ ユウヤ

投資やプログラミング、動画コンテンツの撮影・制作・編集などが得意。元・日本料理の板前。更新のお知らせは、Twitterで。

  • 記事をシェア
© Investment Tech Hack 2021.