どんな記事
Investment Tech Hack
投資歴8年目の兼業トレーダーが運営するサイト。資金管理やリスク管理、複利運用、破産の確率の解説。Pythonで行うバックテストの紹介や、プログラムの公開、自動売買作成の記録など。
investment.abbamboo.com
Gatsby.jsで作っているこのブログに、上記のようなリンクカードを作成する機能を実装しました。
- 同一サイトの投稿ならslug
- 外部サイトならURL
を、指定すると必要な情報を取得してリンクカードを生成します。
参考にしたサイト
元ネタというか、ロジックは以下を参考にしました。
GatsbyJSで作っているブログでリッチなリンクを貼れるようにした
モチベーション アプリやサービスを紹介する際にURLだけだと味気がないので、OGPの情報を拾ってきて、リンク先のタイトルや画像が表示されるようにしたかった noteとかでよく見るやつ できあがったもの こんな感じのやつ。アプリとかサービスの紹介とかで使っていきたい。 検討したサービス まさしく、今回の用途ならIfra…
kikunantoka.com
コーディングにあたっては以下の記事を参考に。
Gatsbyにおける外部取得画像へのgatsby-image適用方法 | Takumon Blog
なにこれGatsbyで画像を扱う場合gatsby-imageを使うとイイ感じに最適化してくれます。Webサイトのリポジトリ内の画像についてはgatsby-config.jsで簡単に設定できますが、今回はビルド時に動的に取得する外部画像に対し...
takumon.com
外部イメージにgatsby-imageを適用する方法
www.deg84.com
GatsbyJSの外部リンクをリッチにする
www.deg84.com
実装にあたって確認したドキュメントは以下です。
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
Gatsby Node APIs
Documentation on Node APIs used in Gatsby build process for common uses like creating pages
www.gatsbyjs.com
RichLinkの機能
同一サイト内のリンク。
10年間変わらない強み 書籍:Strength Finder
書籍「さあ、才能(自分)に目覚めよう Strength Finder 2.0」のまとめと書評です。強みの診断結果をもとに導き出した筆者自身の強みと弱点を掲載しています。筆者本人が、後で内容をさっと確認する備忘録を兼ねています。
books/strength-finder
<RichLink slug="books/strength-finder" title="test" />
外部サイトは、あらかじめlinks.yamlにURLを登録しておく必要がある。 登録されているURLのOGPをもとに生成する。
Investment Tech Hack
投資歴8年目の兼業トレーダーが運営するサイト。資金管理やリスク管理、複利運用、破産の確率の解説。Pythonで行うバックテストの紹介や、プログラムの公開、自動売買作成の記録など。
investment.abbamboo.com
<RichLink href="https://investment.abbamboo.com/" />
独自のテキストを指定することもできる。
test
test
test
<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
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
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で。
- 記事をシェア