From 9c8a22c2b9372079867434ac0d43d5d0d4d1ed93 Mon Sep 17 00:00:00 2001 From: ma91n Date: Fri, 11 Sep 2026 15:21:17 +0900 Subject: [PATCH 01/25] =?UTF-8?q?=E3=81=84=E3=81=84=E3=81=AD=E3=81=AE=20Cl?= =?UTF-8?q?oudflare=20Worker=20=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=99?= =?UTF-8?q?=E3=82=8B=20(#1949)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 記事の「役に立った」を受け取る Worker と KV を workers/likes/ に置く。 数の表示はビルド時に焼き込む方針にしたので、Worker が持つのは POST /like/<記事ID> とバッチ用の GET /dump の2本だけ。 ブログ側の JS と見せ方、毎日のダンプ取り込みは後続のコミットで足す。 Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 35 +++++++++++ workers/likes/.dev.vars.example | 4 ++ workers/likes/.gitignore | 3 + workers/likes/package.json | 11 ++++ workers/likes/src/index.js | 108 ++++++++++++++++++++++++++++++++ workers/likes/wrangler.jsonc | 36 +++++++++++ 6 files changed, 197 insertions(+) create mode 100644 workers/likes/.dev.vars.example create mode 100644 workers/likes/.gitignore create mode 100644 workers/likes/package.json create mode 100644 workers/likes/src/index.js create mode 100644 workers/likes/wrangler.jsonc diff --git a/CLAUDE.md b/CLAUDE.md index 4a957b95afa..99ab36a6f86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,6 +13,7 @@ Hexo 7.3 製の静的サイトで、GitHub Pages にホスティングされて | `themes/future/` | 自作テーマ。`layout/*.ejs`(EJS)と `source/css/`(Stylus) | | `scripts/` | Hexo の generator / helper 拡張(著者ページ、タグ、SNSカウント、OGPプレビューなど) | | `scaffolds/` | `hexo new` のテンプレート | +| `workers/likes/` | いいね機能の Cloudflare Worker(#1949)。Hexo のビルドには関わらない | | `_config.yml` | Hexo 本体設定(permalink、alias によるURLリダイレクト等) | | `themes/future/_config.yml` | テーマ設定(メニュー、GA4 プロパティ、SNSリンク) | | `_profile.yml` | 著者プロフィール(about / twitter_id / github_id) | @@ -609,6 +610,40 @@ make mermaid # mermaid 図のSVGキャッシュ更新(Docker必須、記事の - `.markdownlint-cli2.jsonc`: 行長・生URL・インラインHTMLなどは無効化済み - PR には reviewdog が textlint を回し、変更行にレビューコメントを付ける(`.github/workflows/reviewdog.yml`) +## いいね(Cloudflare Worker) + +記事の「役に立った」を受け取って数を出す(#1949)。**サイトで唯一 JS を使う機能。** +保存先は Cloudflare Worker + KV で、コードは `workers/likes/` が持つ。 + +- **数の表示はビルド時に焼き込む。読者のブラウザからは読まない。** + 記事ページの表示ごとに問い合わせると、全記事ページに外部への通信が1本増える。 + はてブ数も PV も `update-cache.yml` が毎日取って JSON にコミットする形なので、 + いいねもそこへ乗せる。**JS が走るのは押したときだけ** + - 代償は「他人のいいねは翌日反映」。押した本人には POST のレスポンスの最新値を出す。 + 隣に並ぶはてブ数が同じ遅延なので、数の新しさが揃う + - Worker が持つのは `POST /like/<記事ID>` とバッチ用の `GET /dump` の2本だけで、 + 読者向けの `GET /count` は持たない +- **KV のキーは URL ではなく記事ID**(`like:20260804a`)。`_config.yml` の `alias` で + URL を付け替えても数が残る +- **数は値と `metadata` の両方に持つ。** ダンプは `list()` が返す metadata から読むので、 + 記事数ぶんの `get` を撃たずに済む(Workers の無料枠は1リクエストあたり50サブリクエスト + までで、1,500件を `get` で引くと超える)。実測で1,212件が `list()` 2ページ・18KB・32ms +- **Worker は記事の一覧を知らないので、記事IDは形式(`^20\d{6}[a-z]?$`)だけを見る。** + 存在しない記事IDでも書けてしまうが、ダンプを取り込む側が実在する記事だけを拾うので + 表示には出ない。記事IDの一覧を Worker に焼くと、記事を書くたびに再デプロイが要る +- **二重投稿は完全には防げない。** 認証を持たない以上、別ブラウザ・シークレット + ウィンドウからは押せる。技術ブログのいいねとしては許容する + - 端末側は `localStorage`、サーバ側は **IP と記事IDのハッシュ**を KV に TTL 24時間で置く。 + **生の IP は保存しない**(塩を足して SHA-256。記事ごとに別のハッシュになるので、 + 鍵が漏れても「どの端末がどの記事を押したか」の突き合わせにしか使えない) + - **連打と記事IDの総当たりは Rate Limiting binding が止める**(IPあたり60秒で20回)。 + KV を触らないので**書き込み枠を消費しない**。KV の無料枠は書き込みが1日1,000で、 + いいね1回につき2回(カウンタと重複記録)書くため、実質1日500いいねが上限になる + - **`Origin` が一致しないリクエストは受けない。** ブラウザ以外からの POST を + 完全には防げないが、素の叩き方は止まる +- **カウンタの `get` → `put` は競合しうる**(KV は結果整合)。同時に押されると取りこぼすが、 + いいね数は1件の差が読者の判断を変える数字ではないので Durable Objects は使わない + ## ブランチ / デプロイ - 作業は `feature` ブランチで行い、`main` へ PR を出してマージする diff --git a/workers/likes/.dev.vars.example b/workers/likes/.dev.vars.example new file mode 100644 index 00000000000..bfaed6a47e4 --- /dev/null +++ b/workers/likes/.dev.vars.example @@ -0,0 +1,4 @@ +# wrangler dev 用。コピーして .dev.vars を作る(.dev.vars はコミットしない) +ALLOWED_ORIGINS=http://localhost:4000,http://127.0.0.1:4000 +IP_SALT=local-dev-salt +DUMP_TOKEN=local-dev-token diff --git a/workers/likes/.gitignore b/workers/likes/.gitignore new file mode 100644 index 00000000000..18433e3fe1e --- /dev/null +++ b/workers/likes/.gitignore @@ -0,0 +1,3 @@ +.dev.vars +.wrangler/ +node_modules/ diff --git a/workers/likes/package.json b/workers/likes/package.json new file mode 100644 index 00000000000..070e9863d29 --- /dev/null +++ b/workers/likes/package.json @@ -0,0 +1,11 @@ +{ + "name": "future-techblog-likes", + "private": true, + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy" + }, + "devDependencies": { + "wrangler": "^4.131.0" + } +} diff --git a/workers/likes/src/index.js b/workers/likes/src/index.js new file mode 100644 index 00000000000..96d6bc7ebdb --- /dev/null +++ b/workers/likes/src/index.js @@ -0,0 +1,108 @@ +// 記事IDは source/_posts/<年>/YYYYMMDD_*.md の YYYYMMDD + a,b,c… +const POSTID = /^20\d{6}[a-z]?$/; + +// 同じ端末から同じ記事へ押せるのは24時間に1回 +const SEEN_TTL = 60 * 60 * 24; + +export default { + async fetch(request, env) { + const url = new URL(request.url); + const origin = request.headers.get('Origin'); + + if (request.method === 'OPTIONS') { + return new Response(null, { status: 204, headers: cors(env, origin) }); + } + if (request.method === 'POST' && url.pathname.startsWith('/like/')) { + return like(request, env, url.pathname.slice('/like/'.length), origin); + } + if (request.method === 'GET' && url.pathname === '/dump') { + return dump(request, env); + } + return new Response('Not Found', { status: 404 }); + }, +}; + +async function like(request, env, postid, origin) { + if (!allowedOrigin(env, origin)) { + return json({ error: 'forbidden' }, 403, env, origin); + } + // Worker は記事の一覧を知らないので形式だけを見る。存在しない記事IDが書けてしまうが、 + // ダンプを取り込む側が実在する記事だけを拾うので表示には出ない + if (!POSTID.test(postid)) { + return json({ error: 'bad postid' }, 400, env, origin); + } + + const ip = request.headers.get('CF-Connecting-IP') || '0.0.0.0'; + if (env.RATE_LIMITER) { + const { success } = await env.RATE_LIMITER.limit({ key: ip }); + if (!success) return json({ error: 'too many requests' }, 429, env, origin); + } + + const countKey = `like:${postid}`; + const seenKey = `seen:${await fingerprint(env, ip, postid)}`; + const current = Number(await env.LIKES.get(countKey)) || 0; + + if ((await env.LIKES.get(seenKey)) !== null) { + return json({ postid, likes: current, already: true }, 200, env, origin); + } + + const next = current + 1; + // 数を metadata にも持つ。ダンプは list() の metadata から読むので、 + // 記事数ぶんの get を撃たずに済む(Workers 無料枠は1リクエスト50サブリクエストまで) + await env.LIKES.put(countKey, String(next), { metadata: { n: next } }); + await env.LIKES.put(seenKey, '1', { expirationTtl: SEEN_TTL }); + + return json({ postid, likes: next, already: false }, 200, env, origin); +} + +async function dump(request, env) { + if (!env.DUMP_TOKEN || request.headers.get('Authorization') !== `Bearer ${env.DUMP_TOKEN}`) { + return new Response('Unauthorized', { status: 401 }); + } + + const likes = {}; + let cursor; + do { + const page = await env.LIKES.list({ prefix: 'like:', limit: 1000, cursor }); + for (const key of page.keys) { + likes[key.name.slice('like:'.length)] = key.metadata?.n ?? 0; + } + cursor = page.list_complete ? null : page.cursor; + } while (cursor); + + return new Response(JSON.stringify({ likes }), { + headers: { 'Content-Type': 'application/json' }, + }); +} + +// 生の IP は保存しない。記事ごとに別のハッシュになるので、鍵が漏れても +// 「どの端末がどの記事を押したか」の突き合わせにしか使えない +async function fingerprint(env, ip, postid) { + const data = new TextEncoder().encode(`${env.IP_SALT || 'dev'}:${ip}:${postid}`); + const digest = await crypto.subtle.digest('SHA-256', data); + return [...new Uint8Array(digest)] + .slice(0, 16) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +function allowedOrigin(env, origin) { + return Boolean(origin) && env.ALLOWED_ORIGINS.split(',').includes(origin); +} + +function cors(env, origin) { + if (!allowedOrigin(env, origin)) return {}; + return { + 'Access-Control-Allow-Origin': origin, + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Max-Age': '86400', + }; +} + +function json(body, status, env, origin) { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json', ...cors(env, origin) }, + }); +} diff --git a/workers/likes/wrangler.jsonc b/workers/likes/wrangler.jsonc new file mode 100644 index 00000000000..c036d80196b --- /dev/null +++ b/workers/likes/wrangler.jsonc @@ -0,0 +1,36 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "future-techblog-likes", + "main": "src/index.js", + "compatibility_date": "2026-09-01", + "observability": { "enabled": true }, + + "kv_namespaces": [ + { + "binding": "LIKES", + // Cloudflare アカウント作成後に `wrangler kv namespace create LIKES` の出力で置き換える + "id": "PLACEHOLDER_KV_NAMESPACE_ID" + } + ], + + "vars": { + "ALLOWED_ORIGINS": "https://future-architect.github.io" + }, + + // 連打と、記事IDを総当たりして KV の書き込み枠(無料枠は1日1000)を + // 使い切らせる叩き方を止める。KV を使わないので書き込み枠を消費しない + "unsafe": { + "bindings": [ + { + "name": "RATE_LIMITER", + "type": "ratelimit", + "namespace_id": "1001", + "simple": { "limit": 20, "period": 60 } + } + ] + }, + + "dev": { + "port": 8787 + } +} From 4907cd8bed7df55b33ce91daf1ab059ea5d77282 Mon Sep 17 00:00:00 2001 From: ma91n Date: Fri, 11 Sep 2026 16:03:52 +0900 Subject: [PATCH 02/25] =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E3=81=AE?= =?UTF-8?q?=E3=82=B3=E3=83=94=E3=83=BC=E3=81=8C=E5=85=88=E3=81=AB=E5=85=A5?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=84=E3=82=8B=E3=81=AE=E3=81=A7=E3=80=8C?= =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=83=88=E5=88=9D=E3=81=AE=20JS=E3=80=8D?= =?UTF-8?q?=E3=82=92=E7=9B=B4=E3=81=99=20(#1949)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2565(記事末のリンクのコピー)が PR #2730 で公開済みで、 social-button.ejs が既に navigator.clipboard を呼んでいる。 Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 99ab36a6f86..1da7c4f7406 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -612,8 +612,9 @@ make mermaid # mermaid 図のSVGキャッシュ更新(Docker必須、記事の ## いいね(Cloudflare Worker) -記事の「役に立った」を受け取って数を出す(#1949)。**サイトで唯一 JS を使う機能。** -保存先は Cloudflare Worker + KV で、コードは `workers/likes/` が持つ。 +記事の「役に立った」を受け取って数を出す(#1949)。**記事ページで JS を使う2つ目の機能** +(1つ目はリンクのコピー #2565)。保存先は Cloudflare Worker + KV で、 +コードは `workers/likes/` が持つ。 - **数の表示はビルド時に焼き込む。読者のブラウザからは読まない。** 記事ページの表示ごとに問い合わせると、全記事ページに外部への通信が1本増える。 From 0b9671dff6f7957bdf23d9a51d922949365abfbc Mon Sep 17 00:00:00 2001 From: ma91n Date: Fri, 11 Sep 2026 16:36:19 +0900 Subject: [PATCH 03/25] =?UTF-8?q?=E3=81=84=E3=81=84=E3=81=AD=E3=83=9C?= =?UTF-8?q?=E3=82=BF=E3=83=B3=E3=82=92=E5=85=B1=E6=9C=89=E3=83=9C=E3=82=BF?= =?UTF-8?q?=E3=83=B3=E3=81=AE=E9=9A=A3=E3=81=AB=E7=BD=AE=E3=81=8F=20(#1949?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ラベル付きの横長ボタンを共有ボタンと同じ行の左端に置く。色はネイビー、 マークはサムズアップ、高さは丸と同じ 28px。数字はボタンの外。 ハートを使わないのは、一覧と関連記事の ♡ が総シェア数を表していて、 記事末の帯にそのまま並ぶため。 数は like_count_cache.json から焼き込む(段階3でバッチが作る)。 送り先(themes/future/_config.yml の likes_endpoint)が空の間は li が hidden のままなのでボタンは出ない。 Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 29 +++++++ scripts/likes.js | 40 ++++++++++ themes/future/_config.yml | 4 + themes/future/css-src/theme-styles.styl | 75 +++++++++++++++++++ .../future/layout/_partial/social-button.ejs | 69 +++++++++++++++++ themes/future/layout/_partial/svg-icon.ejs | 12 ++- 6 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 scripts/likes.js diff --git a/CLAUDE.md b/CLAUDE.md index 1da7c4f7406..81b2b0d649c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -645,6 +645,35 @@ make mermaid # mermaid 図のSVGキャッシュ更新(Docker必須、記事の - **カウンタの `get` → `put` は競合しうる**(KV は結果整合)。同時に押されると取りこぼすが、 いいね数は1件の差が読者の判断を変える数字ではないので Durable Objects は使わない +**ボタンは共有ボタンと同じ行の左端に置く**(`.like-item`)。外へ出す導線(シェア)ではなく +「ここに残す」操作なので、丸の列との間を一段広く空ける(`ul` の gap 12px + `margin-right` 12px)。 + +- **マークだけにしない。ラベル「役に立った」を持つ。** 共有ボタンはサービスの区別を + アイコンの形が担うので文字を持たないが、いいねは形だけでは押すと何が起きるかが読めない。 + ラベルを持つぶん丸には収まらないので、高さだけ丸と同じ 28px にして上をそろえる +- **マークはサムズアップ**(`thumb-up` / `thumb-up-filled`)。**ハートは使えない**—— + 一覧と関連記事の ♡ は総シェア数で、記事末の帯にそのまま並ぶ。同じ画面で同じ記号が + 2つの意味を持つことになる +- **色は共有ボタンと同じネイビー。クリムゾンにしない。** 1ページに1つなので + 「画面内で同時に1箇所」には抵触しないが、記事末で強く出すぎる。 + #2755 がクリムゾンを却下した2つの理由(表示専用の記号に操作の結果に見える色を与えない/ + ホーム1ページに ♡ が72個)は、**どちらもこのボタンには当たらない**——実際に操作の結果で、 + 1ページに1つ。それでもネイビーにしたのは強さの判断 +- **状態は3つ。押した後と hover を同じ見た目にしない** + - 未押下: 枠 `rule-base`、マークは線画 + - hover / focus: ネイビー反転(共有ボタン・タグチップと同じ型 #2686) + - 押した後: **マークが塗りに変わる**(#2755 の「塗り=自分が押した状態」)。 + 地は白のまま。反転は hover が使っているので、同じにすると2つの状態が割れない +- **数字はボタンの外**(`.share-count` と同じ規則)。0 のときは出さない。 + **件数は読み上げ名に畳む**(#3066 の共有ボタンと同じ) +- **`localStorage` と JS が無いときはボタンごと出さない。** リンクのコピー(#2565)と同じく + `li` に `hidden` を付けておき、送り先が設定されているときだけ script が外す +- **押した瞬間に塗りと +1 を返し、返ってきたらサーバの数で上書きする。** 届かなければ + 見た目も戻す。往復を待たせると押せたかどうかが分からない +- **幅375pxで共有の丸が5つ(2025年7月より前の記事=Pocket あり)のときだけ行が折り返す。** + 合計 355px に対して本文列が 351px。`ul.social-button` はもともと `flex-wrap` を持つので + 丸が1つ下の行に落ちるだけで、そのために間隔を詰めたり段を足したりはしない + ## ブランチ / デプロイ - 作業は `feature` ブランチで行い、`main` へ PR を出してマージする diff --git a/scripts/likes.js b/scripts/likes.js new file mode 100644 index 00000000000..1a3f10709b4 --- /dev/null +++ b/scripts/likes.js @@ -0,0 +1,40 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +/** + * いいねの数(#1949)。 + * + * 数は Cloudflare Worker + KV が持ち、update-cache.yml が毎日ダンプして + * like_count_cache.json にコミットする。表示はここで焼き込むので、 + * 読者のブラウザから数を読みに行くリクエストは発生しない。 + */ + +// permalink の :year:month:day:postid と同じ形。KV のキーもこの ID で、 +// _config.yml の alias で URL を付け替えても数が残る +const POSTID = /^20\d{6}[a-z]?$/; + +// scripts/ はホットリロードされないので、プロセスの生存中は読み直さない +let counts = null; + +function load() { + if (counts) return counts; + try { + const file = path.join(hexo.base_dir, 'like_count_cache.json'); + counts = JSON.parse(fs.readFileSync(file, 'utf8')).likes || {}; + } catch (e) { + // バッチが回るまでは存在しない。0本として描く + counts = {}; + } + return counts; +} + +hexo.extend.helper.register('like_post_id', function (post) { + const matched = /articles\/([^/]+)\//.exec((post && post.path) || ''); + return matched && POSTID.test(matched[1]) ? matched[1] : ''; +}); + +hexo.extend.helper.register('like_count', function (postid) { + return load()[postid] || 0; +}); diff --git a/themes/future/_config.yml b/themes/future/_config.yml index db76321a34b..5c82b173ce1 100644 --- a/themes/future/_config.yml +++ b/themes/future/_config.yml @@ -14,6 +14,10 @@ featured_count: 10 # min_count は旧実装(全期間シェア数順)の閾値で、置き換えに伴い廃止した tag_display_max_count: 10 +# いいねの送り先 (#1949)。Cloudflare Worker の URL。 +# 空なら押しても何も起きないのでボタンごと出さない +likes_endpoint: + # Integrations(GA4 プロパティ) google_analytics: G-X1C28R8H0M diff --git a/themes/future/css-src/theme-styles.styl b/themes/future/css-src/theme-styles.styl index 13e2cccf86e..a16636d47ec 100644 --- a/themes/future/css-src/theme-styles.styl +++ b/themes/future/css-src/theme-styles.styl @@ -2781,6 +2781,81 @@ button.share-btn { font-variant-numeric: tabular-nums; } +/* いいね (#1949)。共有の丸と同じ行の左端に置く。外へ出す導線(シェア)ではなく + 「ここに残す」操作なので、丸の列との間を一段広く空ける(gap 12px + 12px)。 + + マークだけにしない。共有ボタンはサービスの区別をアイコンの形が担うので + ラベルを持たないが、いいねは形だけでは押すと何が起きるかが読めない。 + + 色は共有ボタンと同じネイビー。クリムゾンは差し色で役が3つ埋まっており、 + 1ページに1つだけとはいえ記事末で強く出すぎる */ +.like-item { + display: flex; + align-items: center; + gap: 8px; + /* 丸の列は「丸+その下の数字」で背が高い。上をそろえれば中心もそろう */ + align-self: flex-start; + margin-right: 12px; +} +.like-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + /* 共有の丸と同じ高さ */ + height: 28px; + padding: 0 14px; + font: inherit; + font-size: text-row; + line-height: 1; + color: var(--brand-navy); + background: transparent; + border: 1px solid var(--rule-base); + border-radius: 999px; + box-sizing: border-box; + cursor: pointer; + white-space: nowrap; + transition: background-color hover-speed ease, border-color hover-speed ease, + color hover-speed ease; +} +/* 共有ボタン・タグチップと同じネイビー反転。小さな操作部品の反応を + 1種類にそろえる (#2686) */ +.like-button:hover, +.like-button:focus-visible { + background: var(--brand-navy); + border-color: var(--brand-navy); + color: var(--on-dark-strong); +} +/* 押した後はマークの塗りで示す (#2755)。地は白のまま——反転は hover が + 使っているので、同じ見た目にすると「押した」と「指している」が割れない */ +.like-button[aria-pressed="true"] { + border-color: var(--brand-navy); + cursor: default; +} +/* 既定の .svg-icon は文字に添える前提で 1em + 右マージンを持っている */ +.like-button .svg-icon { + width: 16px; + height: 16px; + flex: none; + margin-right: 0; + vertical-align: 0; +} +.like-icon-filled, +.like-button[aria-pressed="true"] .like-icon-outline { + display: none; +} +.like-button[aria-pressed="true"] .like-icon-filled { + display: block; +} +/* 数字はボタンの外。押せる範囲と数字が別であることを位置で示す + (.share-count と同じ規則) */ +.like-count { + color: var(--brand-navy); + font-size: text-meta; + line-height: 1; + font-variant-numeric: tabular-nums; +} + /* アイコンだけのリンク(GitHub / X / Youtube / RSS)。フッターと著者ページの プロフィール (#2774) で共有する。下線は文字のための印なので消し、反応は チップと同じ「地を一段」で示す。タップ領域は Lighthouse の diff --git a/themes/future/layout/_partial/social-button.ejs b/themes/future/layout/_partial/social-button.ejs index f14fbaddd86..e74e14d67f9 100644 --- a/themes/future/layout/_partial/social-button.ejs +++ b/themes/future/layout/_partial/social-button.ejs @@ -37,6 +37,32 @@ }; %>