Skip to content
気ままに、ちまちま。テックと日常。

【HubSpot】カードリンク(ブログカード)を作ってみよう 🔗

【HubSpot】カードリンク(ブログカード)を作ってみよう 🔗
2:26
目次

やりたかったこと 💭

HubSpotのブログやページで、URLを入力すると自動でOGP情報(タイトル・画像)を取得して、カードスタイルで表示するモジュールを作りたかった!

⭐いわゆる カードリンク(ブログカード) みたいなやつ

🛠 やったことまとめ

① サーバーレス関数を使って OGP 情報を取得

👇の記事にも書かれているように、フロントエンド側だけだと CORS エラーが起きてしまうのでサーバサイドの処理が必要のよう… (あんまりわかってないけど)

 

今回は、HubSpot のサーバーレス関数 を使って、指定したURLの OGP データを取得する流れを作った!

  • HubSpot のサーバーレス関数(Node.js)を設定
  • fetch でページの OGP 情報(og:title, og:image)を取得
  • CORS の問題を回避するためにサーバーレス関数経由でリクエスト

📍この記事で書かれているコードは、開発者ドキュメントと ChatGPT に聞きながら書いたものなので「自分で書けるよ!」って人はもっと素敵に構築してください~

✅ serverless.json の設定

{"runtime":"nodejs18.x","version":"1.0","endpoints":{"fetch-ogp":{"method":"GET","file":"fetch-ogp.js"}}}

✅ fetch-ogp.js のコード(OGP取得関数)

exports.main = async (context, sendResponse) => { const url = context.params.url; if (!url) { sendResponse({ statusCode: 400, body: "URLが必要です" }); return; } try { const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0" } }); const html = await response.text(); sendResponse({ statusCode: 200, body: html, headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" } }); } catch (error) { sendResponse({ statusCode: 500, body: "OGP取得失敗" }); } };

このエンドポイントを https://自分のドメイン/_hcms/api/fetch-ogp?url=取得したいページのURL で呼び出せるようにした!

② カスタムモジュールで OGP カードを表示する

HubSpotのカスタムモジュールで、取得したOGPデータをカードスタイルで表示!

カスタムモジュールのフィールドは

  1. ページURL(URL)
  2. ページテキスト(テキスト)
  3. サムネイル画像(画像)

の3つで構成

✅ module.html

<div class="ogp-card" data-page-url="{{ module.page_url }}" data-fallback-title="{{ module.fallback_title }}" data-fallback-image="{{ module.fallback_image.src }}"> <a class="og-link" href="#" target="_blank"> <img class="og-image" src="" alt="OGP画像"> <div class="card-text"> <h3 class="og-title"></h3> </div> </a> </div>

 

✅ module.css

/* 📌 OGPカード全体 */ .ogp-card { display: flex; align-items: center; text-decoration: none; color: #333; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.1); transition: transform 0.2s, box-shadow 0.2s; width: 100%; max-width: 600px; /* 最大幅を設定 */ margin: 10px auto; /* カード同士に余白をつけて中央寄せ */ border: 1px solid #ddd; /* グレーの枠線 */ } /* 📌 OGPカードのレイアウト */ .ogp-card a { display: flex; width: 100%; align-items: center; text-decoration: none; } /* 📌 サムネイル画像 */ .ogp-card .og-image { width: 130px; /* 画像サイズ大きめ */ height: 130px; object-fit: cover; border-radius: 8px 0 0 8px; } /* 📌 テキストエリア */ .ogp-card .card-text { padding: 10px 15px; flex-grow: 1; display: flex; align-items: center; } /* 📌 タイトル */ .ogp-card .og-title { font-size: 18px; font-weight: bold; line-height: 1.4; text-decoration: none; border-bottom: 2px solid transparent; transition: border-color 0.2s ease-in-out; } /* 🎯 📱 スマホ対応(横並び → 縦並びに変更) */ @media (max-width: 600px) { .ogp-card a { flex-direction: column; /* 縦並びにする */ align-items: flex-start; } .ogp-card .og-image { width: 100%; /* 幅いっぱいに */ height: auto; border-radius: 8px 8px 0 0; } .ogp-card .card-text { padding: 12px; } .ogp-card .og-title { font-size: 16px; /* スマホでは少し小さく */ } }

✅ module.js (Portal ID は伏せてます!)

document.addEventListener("DOMContentLoaded", function () { console.log("window.fetchOGP の状態:", window.fetchOGP); if (typeof window.fetchOGP !== "function") { console.error("fetchOGP 関数が定義されていません!"); return; } const HUBSPOT_DOMAIN = window.location.origin; const PORTAL_ID = "XXXXXX"; const FUNCTION_PATH = "/_hcms/api/fetch-ogp"; document.querySelectorAll(".ogp-card").forEach((ogpCard, index) => { let pageUrlData = ogpCard.dataset.pageUrl; console.log(`取得したページURLデータ (${index + 1}):`, pageUrlData); let finalUrl = ""; const hrefMatch = pageUrlData.match(/href=(https?:\/\/[^\s,]+)/); if (hrefMatch) { finalUrl = hrefMatch[1]; } else { console.warn(`ページURLを解析できませんでした (${index + 1})。デフォルトの値を使用します。`); finalUrl = pageUrlData; } console.log(`最終的なページURL (${index + 1}):`, finalUrl); if (!finalUrl || finalUrl.trim() === "") { console.error(`ページURLが空です!リクエストを送信できません (${index + 1})。`); return; } const API_URL = `${HUBSPOT_DOMAIN}${FUNCTION_PATH}?portalId=${PORTAL_ID}&url=${encodeURIComponent(finalUrl)}`; const fallbackTitle = ogpCard.dataset.fallbackTitle; const fallbackImage = ogpCard.dataset.fallbackImage; window.fetchOGP(API_URL, fallbackTitle, fallbackImage, ogpCard); }); }); window.fetchOGP = async function (apiUrl, fallbackTitle, fallbackImage, ogpCard) { try { console.log(`OGP取得開始: ${apiUrl}`); const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`HTTPエラー: ${response.status}`); } const textData = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(textData, "text/html"); let title = doc.querySelector('meta[property="og:title"]')?.content || fallbackTitle; let image = doc.querySelector('meta[property="og:image"]')?.content || fallbackImage; ogpCard.querySelector(".og-title").textContent = title; ogpCard.querySelector(".og-image").src = image; ogpCard.querySelector(".og-link").href = apiUrl; console.log(`OGP取得成功: タイトル="${title}", 画像="${image}"`); } catch (error) { console.error("OGP情報の取得に失敗しました", error); ogpCard.querySelector(".og-title").textContent = fallbackTitle; ogpCard.querySelector(".og-image").src = fallbackImage; } };

 

💡 まとめ

結果的に👇の形で、モジュールの [URL]フィールドに指定されたページの OGP 情報から ページタイトル・サムネイル画像を引っ張ってきて表示させるカスタムモジュールを作れた⛅

 

もし、OGPで取得できなかった場合はフィールドに手動で指定することもできるのでそっちの情報を出すことも可能

card-link

 

✅ HubSpotのサーバーレス関数を使ってOGP情報を取得

✅ カスタムモジュールでOGP情報をカード形式で表示(複数設置にも対応!)

✅ レスポンシブ対応・デザイン調整


サーバーレス関数は HubSpot CLI 使うことが前提だから、ちょっととっつきにくいけど HubSpotのカスタムモジュールって、サーバーレス関数を組み合わせるとこんなこともできるんだな〜って実感💡✨ また他にもいろいろ試してみたいなって思った🔥

Reina

Written by Reina

HubSpot CMS (現 Content Hub) をメインに 気になったことをまとめます。猫が好きです