diff --git a/site/src/components/tools/ToolFrame.tsx b/site/src/components/tools/ToolFrame.tsx
index 3371d4c4..69cb4362 100644
--- a/site/src/components/tools/ToolFrame.tsx
+++ b/site/src/components/tools/ToolFrame.tsx
@@ -11,6 +11,7 @@ import { ShareBar } from "@/components/tools/ShareBar";
import { FavoriteButton } from "@/components/tools/FavoriteButton";
import { siteConfig } from "@/lib/config";
import { CATEGORY_CONFIG } from "@/lib/tools/categories";
+import { MANUAL_SHARE_SLUGS } from "@/lib/tools/manual-share";
import type { ToolMeta } from "@/lib/tools/types";
interface Props {
@@ -54,7 +55,12 @@ export function ToolFrame({ meta, title, description, related, children, article
押し出されていた(個別ツール着地の engaged≒0% の構造要因)。
まず道具を見せ、広告は結果の下に置く(2026-08-22)。 */}
diff --git a/site/src/components/tools/ToolInteractionTracker.tsx b/site/src/components/tools/ToolInteractionTracker.tsx
index 14aab9e1..7718c89f 100644
--- a/site/src/components/tools/ToolInteractionTracker.tsx
+++ b/site/src/components/tools/ToolInteractionTracker.tsx
@@ -2,6 +2,7 @@
import { useEffect, useRef, type ReactNode } from "react";
import { trackCalculate, trackCopyResult, trackPresetClick } from "@/lib/analytics/events";
+import { useAutoShareableState } from "@/lib/hooks/useAutoShareableState";
/**
* ツール本体を包み、GA4 の calculate / copy_result / preset_click を一括計装する。
@@ -25,6 +26,7 @@ export function ToolInteractionTracker({
slug,
locale,
category,
+ shareable = true,
children,
}: {
slug: string;
@@ -32,6 +34,11 @@ export function ToolInteractionTracker({
locale?: string;
/** ToolMeta.category。GA4 custom dimension `category` に載せる。 */
category?: string;
+ /**
+ * 入力値を ?s= に載せる汎用共有リンクを有効にするか。
+ * ツール自身が useShareableState を持つ場合は false(二重書き込み回避)。
+ */
+ shareable?: boolean;
children: ReactNode;
}) {
const ref = useRef(null);
@@ -82,5 +89,7 @@ export function ToolInteractionTracker({
};
}, [slug, locale, category]);
+ useAutoShareableState(ref, slug, shareable);
+
return {children}
;
}
diff --git a/site/src/lib/hooks/useAutoShareableState.ts b/site/src/lib/hooks/useAutoShareableState.ts
new file mode 100644
index 00000000..6f6a6b6b
--- /dev/null
+++ b/site/src/lib/hooks/useAutoShareableState.ts
@@ -0,0 +1,233 @@
+"use client";
+
+import { useEffect, type RefObject } from "react";
+
+/**
+ * ツール本体の DOM 入力を丸ごと ?s= に載せ、共有リンクから復元する。
+ *
+ * なぜ DOM 総なめ方式か: useShareableState は (slug, value, restore) を
+ * ツール側が手書きする契約で、223本中7本しか採用されていない。残り216本に
+ * 個別の restore コールバックを書くのは非現実的で、実装差異による取りこぼしも
+ * 避けられない。入力の実体はどれも DOM 上の input/select/textarea なので、
+ * ToolInteractionTracker と同じく「バブリングを1箇所で拾う」方が網羅的。
+ *
+ * キーは「出現位置」ではなく構造キー(type + 同種内の連番)。理由は
+ * triangle-calculator のように mode によって表示される input 本数が変わる
+ * ツールが35本あり、素の通し番号だと復元先が1つずれて別の項目に値が入るため。
+ * 構造キーなら、表示中のフィールド群が変わっても同種フィールドの対応が保たれる。
+ *
+ * 復元は2パスで行う。select(モード切替)を先に当ててから、それによって
+ * 新しく現れた input を次のフレームで埋める。1パスだと「sss モードの共有リンクを
+ * right モードの初期表示に流し込む」ことになり、値が入らない/誤った欄に入る。
+ *
+ * React 19 の制御コンポーネントは el.value への直接代入を無視する(内部の
+ * value tracker が「変化なし」と判断して onChange を出さない)ため、
+ * ネイティブ setter で書いてから input/change を dispatch する。
+ *
+ * モード切替が