これまでの Next.js ブログサイト構築シリーズをまとめたページは以下の通りです。
記事を読んでくれた人が「これは役に立った」「友達にも教えたい」と思ったとき、
シェアボタンがなければその気持ちはそのまま消えてしまいます。
SNS シェアボタンは、読者の「シェアしたい」という気持ちを
その場で行動に変えるための仕組みです。
また、シェアされた記事は SNS 上で新たな読者の目に触れます。
広告費ゼロで記事が拡散される可能性があるという意味で、
シェアボタンはブログの集客にも直結する機能です。
この記事では、Next.js で構築した個別記事ページに
X(Twitter)・はてなブックマーク・LINE・URL コピーの
4つのシェアボタンを追加する手順を解説します。
外部の SNS サービスのシェア用 URL を利用するため、API キーは不要です。react-icons でアイコンを表示し、Tailwind CSS でレイアウトを整えます。
この記事を読むと、以下のことができるようになります。
react-iconsを使ってアイコン付きのシェアボタンを作れる- PC は4列横一列・スマホは2×2グリッドのレスポンシブなレイアウトが作れる
- コンポーネントを1つ作って、記事の上部・下部の2箇所で使い回せる
- コピーボタンに「コピーしました!」のフィードバック表示を付けられる
実装する機能の仕様
今回実装するシェアボタンの仕様は以下の通りです。
| 項目 | 内容 |
|---|---|
| シェア先 | X・はてブ・LINE・コピーの4つ |
| 設置場所 | アイキャッチ画像の下(上部)と記事本文の下(下部)の2箇所 |
| 呼びかけテキスト | 下部のみ「この記事が役に立ったら、シェアしていただけると嬉しいです!」 |
| レイアウト | PC(768px以上)は4列横一列、スマホは2×2グリッド |
| ボタンカラー | 各 SNS のブランドカラー |
| コピーフィードバック | 「コピーしました!」を2秒表示後に元に戻る |
Facebook を採用しなかった理由: 総務省の調査(令和6年度)によると、日本における Facebook の全年代利用率は 26.8% まで低下しています。技術系ブログとの相性も高くないため、今回は見送りました。
Step 1:react-icons をインストールする
アイコン表示のために react-icons パッケージをインストールします。
cd ~/example-blog && npm install react-icons
react-icons とは、X・LINE・はてブなどさまざまなサービスのアイコンを React で簡単に使えるようにしたライブラリです。
自分でアイコンの形をコードで書く必要がなく、名前を指定するだけで表示できます。
以下のような表示が出れば成功です。
added 1 package, and audited 515 packages in Xs
2 moderate severity vulnerabilities という警告が出ることがありますが、今回の作業には影響しません。
Step 2:ShareButtons コンポーネントを作成する
シェアボタン全体を1つのコンポーネントとして作成します。
コンポーネントを1つにまとめることで、記事上部・下部の両方で同じ部品を使い回せます。
まず新規ファイルを作成します。
touch ~/example-blog/components/ShareButtons.tsx
作成したファイルを開き、以下のコードを貼り付けてください。
'use client'
import { useState } from 'react'
import { FaXTwitter, FaLine } from 'react-icons/fa6'
import { SiHatenabookmark } from 'react-icons/si'
import { FiCopy, FiCheck } from 'react-icons/fi'
type Props = {
url: string
title: string
showMessage?: boolean
}
export default function ShareButtons({ url, title, showMessage = false }: Props) {
const [copied, setCopied] = useState(false)
const encodedUrl = encodeURIComponent(url)
const encodedTitle = encodeURIComponent(title)
const handleCopy = async () => {
await navigator.clipboard.writeText(url)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const shareButtons = [
{
label: 'X',
icon: <FaXTwitter size={18} />,
href: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`,
className: 'bg-black hover:bg-gray-800',
},
{
label: 'はてブ',
icon: <SiHatenabookmark size={18} />,
href: `https://b.hatena.ne.jp/entry/s/${url.replace('https://', '')}`,
className: 'bg-[#00A4DE] hover:bg-[#0090c4]',
},
{
label: 'LINE',
icon: <FaLine size={18} />,
href: `https://social-plugins.line.me/lineit/share?url=${encodedUrl}`,
className: 'bg-[#06C755] hover:bg-[#05b34b]',
},
]
return (
<div className="my-6">
{showMessage && (
<p className="text-center text-sm text-gray-400 mb-3">
この記事が役に立ったら、シェアしていただけると嬉しいです!
</p>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{shareButtons.map((btn) => (
<a
key={btn.label}
href={btn.href}
target="_blank"
rel="noopener noreferrer"
className={`flex items-center justify-center gap-2 ${btn.className} text-white text-sm font-medium py-2 px-3 rounded transition-colors`}
>
{btn.icon}
<span>{btn.label}</span>
</a>
))}
<button
onClick={handleCopy}
className={`flex items-center justify-center gap-2 text-white text-sm font-medium py-2 px-3 rounded transition-colors ${
copied ? 'bg-green-600' : 'bg-gray-600 hover:bg-gray-500'
}`}
>
{copied ? <FiCheck size={18} /> : <FiCopy size={18} />}
<span>{copied ? 'コピーしました!' : 'コピー'}</span>
</button>
</div>
</div>
)
}
コードの解説
import 文:必要な部品を取り込む
import { useState } from 'react'
import { FaXTwitter, FaLine } from 'react-icons/fa6'
import { SiHatenabookmark } from 'react-icons/si'
import { FiCopy, FiCheck } from 'react-icons/fi'
各行で必要な部品を取り込んでいます。
useState:ボタンを押したときの状態(コピー済み/未コピー)を管理する仕組みFaXTwitter・FaLine:X と LINE のアイコン(react-icons/fa6というアイコンセットより)SiHatenabookmark:はてブのアイコン(react-icons/siというアイコンセットより)FiCopy・FiCheck:コピーボタンのアイコン(通常時とコピー済み時で切り替える)
'use client' の宣言
このコンポーネントはコピーボタンの状態管理に useState を使うため、クライアントコンポーネントとして宣言する必要があります。
Next.js では、ブラウザ上でのクリック操作や状態変化を扱うコンポーネントには 'use client' を先頭に記述します。
Props(受け取る情報)の定義
type Props = {
url: string
title: string
showMessage?: boolean
}
このコンポーネントが外部から受け取る情報を定義しています。
url:シェアする記事の URL(必須)title:シェアする記事のタイトル(必須)showMessage:呼びかけテキストを表示するかどうか(?が付いているため省略可能・省略時はfalse)
URL エンコードの処理
const encodedUrl = encodeURIComponent(url)
const encodedTitle = encodeURIComponent(title)
URL やタイトルをシェア用 URL に埋め込める形式に変換する処理です。
日本語などの特殊文字が含まれていても正しく動作するよう、「URL エンコード」という変換を行います。
コピー処理と2秒フィードバック
const handleCopy = async () => {
await navigator.clipboard.writeText(url)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
コピーボタンをクリックしたときの処理です。
async と await は「時間がかかる処理が終わるまで待つ」という意味の書き方です。
クリップボードへの書き込みは一瞬ですが、処理が確実に完了してから次の行に進むよう await を付けています。
書き込み完了後、setCopied(true) で「コピーしました!」表示に切り替え、2秒後に setCopied(false) で元の表示に戻します。
シェアボタンを配列で管理する理由
const shareButtons = [
{ label: 'X', icon: ..., href: ..., className: ... },
{ label: 'はてブ', icon: ..., href: ..., className: ... },
{ label: 'LINE', icon: ..., href: ..., className: ... },
]
X・はてブ・LINE の3つのボタン情報を配列にまとめています。
ラベル・アイコン・シェア先 URL・色をセットで管理することで、将来ボタンを追加・変更する際に1箇所を修正するだけで済みます。
各 SNS のシェア用 URL は以下の仕組みです。
- X:
https://twitter.com/intent/tweet?url=記事URL&text=記事タイトル - はてブ:
https://b.hatena.ne.jp/entry/s/記事URLのhttps://を除いた部分 - LINE:
https://social-plugins.line.me/lineit/share?url=記事URL
レスポンシブなグリッドレイアウト
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
grid-cols-2 でスマホは2列、md:grid-cols-4 で PC(768px 以上)は4列になります。
この1行で PC・スマホの両方に対応できます。
target="_blank" rel="noopener noreferrer" について
シェアボタンをクリックしたとき、新しいタブで SNS のシェア画面が開くようにしています。rel="noopener noreferrer" はセキュリティ対策として必須の記述です。
これがないと、開いた先のページから元のページの情報にアクセスされる可能性があります。
コピーボタンだけ <button> を使っている理由
X・はてブ・LINE は外部 URL へのリンクなので <a> タグを使います。
コピーボタンはどこかへ遷移するのではなく、JavaScript の処理を実行するだけなので <button> タグを使います。
用途に応じてタグを使い分けることが重要です。
Step 3:個別記事ページに組み込む
~/example-blog/app/posts/[id]/page.tsx を開いてください。
code ~/example-blog/app/posts/[id]/page.tsx
以下の3箇所を追加します。
① import の追加
ファイル先頭の import 群(他の import 文が並んでいる部分)に以下1行を追加します。
import { getPostData, getSortedPostsData, getRelatedPosts } from '../../../lib/posts';
import ShareButtons from '../../../components/ShareButtons'; // ← この行を追加
import ReactMarkdown from 'react-markdown';
// (以下、既存の import が続く)
② 上部シェアボタンの追加
アイキャッチ画像ブロックの直後、記事本文ブロックの直前を探してください。
{/* ── アイキャッチ画像 ───────────────────────────── */}
{postData.ogImage && postData.ogImage !== '' && (
<div className="relative w-full aspect-[1200/630]">
{/* (画像の中身)*/}
</div>
)}
{/* ↓ここに追加する↓ */}
{/* ── 記事本文 ───────────────────────────── */}
追加するコードはこちらです。
{/* ── 上部シェアボタン ───────────────────────────── */}
<div className="px-6 pt-4">
<ShareButtons
url={`${SITE_URL}/posts/${postData.id}`}
title={postData.title}
/>
</div>
showMessage を省略しているため、呼びかけテキストは非表示になります。
③ 下部シェアボタンの追加
記事本文ブロックの直後、</article> の直前を探してください。
{/* ── 記事本文 ───────────────────────────── */}
<div className="p-6 prose prose-invert max-w-none">
{/* (記事本文の中身)*/}
</div>
{/* ↓ここに追加する↓ */}
</article>
追加するコードはこちらです。
{/* ── 下部シェアボタン ───────────────────────────── */}
<div className="px-6 pb-6">
<ShareButtons
url={`${SITE_URL}/posts/${postData.id}`}
title={postData.title}
showMessage={true}
/>
</div>
showMessage={true} を渡すことで、呼びかけテキストが表示されます。
Step 4:ビルドして動作確認する
cd ~/example-blog && npm run build
エラーが出なければ成功です。
Step 5:デプロイする
./deploy.sh
デプロイ後、ブラウザで個別記事ページを開き、以下を確認してください。
- アイキャッチ画像の下にシェアボタンが表示されているか
- 記事本文の下に呼びかけテキストとシェアボタンが表示されているか
- コピーボタンを押すと「コピーしました!」と表示され、2秒後に「コピー」に戻るか
- 各 SNS ボタンをクリックすると新しいタブでシェア画面が開くか

サイトの個別記事を開くと、アイキャッチ画像の下で、本文記事よりも上にシェアボタンが4つ並んで表示されています。

個別記事の最後には、「この記事が役に立ったら、シェアいただけると嬉しいです!」のメッセージとともに、再びシェアボタンが表示されます。
まとめ
今回は個別記事ページに SNS シェアボタンを実装しました。
react-icons を使うことでアイコンの実装が簡単になり、シェア用 URL は各 SNS が公開しているものを利用するだけなので API キーも不要です。
1つのコンポーネントを作って上部・下部で使い回す設計にしたことで、コードの重複もなくすっきり管理できます。
OGP はシリーズ第9回で設定済みのため、X や LINE でシェアされた際にアイキャッチ画像・タイトル・説明文が自動的に表示されます。
シェアボタンを置くだけで、見た目にもきれいなシェアカードとして拡散される状態が整っています。
実装したものの、実際にシェアボタンを教えてもらえるかどうかはわかりません。
シェアしたくなるような役に立つ、面白い記事をお届けできればと思っていますが、まずは、シェアしようと思ってもらえたときに、簡単にシェアしてもらえる仕組みを作れたのは良かったです。
先の話になるかもしれませんが、シェアボタンが押されたときに、どのページで、どのボタンが何回押されたのかをカウントできるようにしても面白いかもしれませんね。
全然押してもらえず、悲しい思いをするかもしれませんが、Next.js で自分の好きなようにブログサイトを開発しているので、個人的な好みは、どんどん反映させていきたいと思います。


コメント