FAQ③検索意図別(結論先出し・構造化データ同梱)
質問を絞り込まず、読者の検討段階(比較検討中/発注直前/契約後)で束ねるFAQブロック。質問文は検索されている語をそのまま併記し、回答は先頭1文で結論を言い切る——その1文は閉じた状態でも常時見えていて、開くのは根拠と前提条件だけ。段階ごとに「まとめて開く」があり、開いている件数が控えめに出る。回答テキストは表示と同じ配列から組み立てたFAQPageのJSON-LDになり、出力されるJSONは画面下部でそのまま確認できる(実案件ではpropひとつでscriptタグとして出力)。開閉はネイティブの<details>のままなので、閉じた回答も本文がDOMに残りページ内検索に当たる。段階ラベルの列は狭幅で上へ積み替わる。角丸ゼロ・画像素材ゼロ・依存ゼロ。掲載している金額や期間はすべて表示サンプル。
- 追加日:
- 2026-09-27
- 依存:
- なし
- tags
- #faq #accordion #details #search-intent #structured-data #json-ld #seo #hairline #container-query #no-image
プレビュー
質問と回答
検討の段階ごとに、結論から答えます
いただく質問を、比較検討中・発注直前・契約後の3段階に分けました。回答の1行目が結論で、その根拠と前提は必要なときだけ開けます。全9問。
比較検討中
相場を知り、数社を並べて外すところを探している段階。金額と判断基準を先に出す。
コーポレートサイト 制作費 相場
制作費はいくらくらいかかりますか
企業サイトは80万〜250万円が中心で、金額はページ数と原稿・写真のご用意状況でほぼ決まります。
根拠と前提を開く閉じる
- 10ページ前後・原稿ご支給で80万円台、取材と撮影を含めて20ページ規模になると200万円を超えます。
- 更新のしくみ(管理画面から編集できる範囲)を広げると、ページ数よりも金額が動きます。
- 初回のご相談から概算のご提示までは費用をいただきません。
制作会社 比較 選び方
他社と比べるとき、どこを見れば違いが分かりますか
見積書の「デザイン費」の内訳と、公開後1年に含まれる作業範囲を並べると差が出ます。
根拠と前提を開く閉じる
- デザイン費が1行なら、個別に設計する画面が何枚かを聞いてください。枚数で工数が決まります。
- 公開後の初期不具合対応・軽微な文言修正が有償か無償かは会社ごとに違います。
- 運用を自社で回す前提なら、管理画面の操作手順書が納品物に入っているかを確認してください。
ホームページ 作っても 効果ない
作っても問い合わせが増えないのではと不安です
先に「月に何件増えれば成功か」を決め、その数字に効く導線だけを作ります。
根拠と前提を開く閉じる
- 目標件数が決まると、必要なページと不要なページが分かれるので見積も小さくできます。
- 公開後3か月は流入元と離脱箇所を計測し、原稿の差し替えで改善します。
- 検索からの流入が見込めない業種では、広告や紹介からの着地面として設計します。
発注直前
社内を通すために数字と条件を固める段階。見積の読み方と後戻りの条件を隠さない。
見積書 内訳 削れる項目
見積の項目が多く、どこを削れるか分かりません
削るならページ数と撮影、削らないほうがよいのは設計と公開前の検証です。
根拠と前提を開く閉じる
- 下層ページは第2期にまわせます。公開後の追加でも単価はほぼ変わりません。
- 写真は既存素材の流用でも成立します。撮影は必要な箇所を絞ってご提案します。
- 構成設計と表示・フォームの検証を削ると、公開後の修正費が上回ることが多いです。
ホームページ制作 期間 どのくらい
いつ公開できますか。逆算して発注日を決めたいです
標準的な企業サイトで発注から2〜3か月、原稿がそろっていれば最短6週間です。
根拠と前提を開く閉じる
- 内訳は構成設計に2週間、デザインに3週間、実装と検証に3週間が目安です。
- 止まりやすいのは原稿の確認待ちです。確認の担当と締切を先に決めておくと短縮できます。
- 公開希望日が決まっている場合は、そこから逆算した工程表をお見積りに添えます。
制作 途中解約 費用
途中でやめることになったら費用はどうなりますか
完了している工程までの実費精算で、未着手分は着手金からご返金します。
根拠と前提を開く閉じる
- 工程ごとの金額を契約書に明記するので、途中で止めたときの金額が事前に分かります。
- 設計書・デザインデータなど、その時点までの成果物はお渡しします。
- 担当者の異動や方針変更での中断も、追加の違約金はいただきません。
契約後・公開後
自社で運用できるかを確かめる段階。連絡先と月額の中身を具体的に書く。
ホームページ 自分で更新 できる
公開後、社内で更新できますか
お知らせ・実績・スタッフ紹介は、管理画面から追加と並べ替えまでできます。
根拠と前提を開く閉じる
- レイアウトが崩れない範囲を編集画面側で制限するので、HTMLの知識は要りません。
- 操作手順書のお渡しと、担当者向けの説明を1時間ほど行います(初回は費用に含みます)。
- デザインの変更やページの新設は、こちらで承ります。
サイト 表示崩れ 問い合わせ先
不具合が出たときはどこへ連絡すればいいですか
制作を担当した者へ直接ご連絡ください。平日は当日中に一次回答します。
根拠と前提を開く閉じる
- メール・電話・チャットのいずれでも受け付けます。窓口は公開後も変わりません。
- 表示崩れとフォームの不着は、原因の切り分けまで無償で対応します。
- サーバー障害など当社以外に原因がある場合も、状況の確認と連絡を代行します。
サイト 保守費用 内容
月額の保守費は何に使われますか
更新代行の時間・脆弱性対応・バックアップ・月次の計測レポートの4つです。
根拠と前提を開く閉じる
- 更新代行は月2時間まで。超える月は事前にご相談のうえ時間単位で精算します。
- 土台のソフトウェア更新と表示確認を月1回行い、結果を記録して共有します。
- 計測レポートには流入元・よく読まれたページ・問い合わせ件数と、次の打ち手を1つ書きます。
このFAQが出力する構造化データ(FAQPage)
画面に出ている質問と回答から組み立てたJSON-LDです。回答は結論1文と根拠を連結した1つの acceptedAnswer になります。本文と同じ配列から作るので、文言を直すと出力も一緒に変わります。
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "制作費はいくらくらいかかりますか",
"acceptedAnswer": {
"@type": "Answer",
"text": "企業サイトは80万〜250万円が中心で、金額はページ数と原稿・写真のご用意状況でほぼ決まります。 10ページ前後・原稿ご支給で80万円台、取材と撮影を含めて20ページ規模になると200万円を超えます。 更新のしくみ(管理画面から編集できる範囲)を広げると、ページ数よりも金額が動きます。 初回のご相談から概算のご提示までは費用をいただきません。"
}
},
{
"@type": "Question",
"name": "他社と比べるとき、どこを見れば違いが分かりますか",
"acceptedAnswer": {
"@type": "Answer",
"text": "見積書の「デザイン費」の内訳と、公開後1年に含まれる作業範囲を並べると差が出ます。 デザイン費が1行なら、個別に設計する画面が何枚かを聞いてください。枚数で工数が決まります。 公開後の初期不具合対応・軽微な文言修正が有償か無償かは会社ごとに違います。 運用を自社で回す前提なら、管理画面の操作手順書が納品物に入っているかを確認してください。"
}
},
{
"@type": "Question",
"name": "作っても問い合わせが増えないのではと不安です",
"acceptedAnswer": {
"@type": "Answer",
"text": "先に「月に何件増えれば成功か」を決め、その数字に効く導線だけを作ります。 目標件数が決まると、必要なページと不要なページが分かれるので見積も小さくできます。 公開後3か月は流入元と離脱箇所を計測し、原稿の差し替えで改善します。 検索からの流入が見込めない業種では、広告や紹介からの着地面として設計します。"
}
},
{
"@type": "Question",
"name": "見積の項目が多く、どこを削れるか分かりません",
"acceptedAnswer": {
"@type": "Answer",
"text": "削るならページ数と撮影、削らないほうがよいのは設計と公開前の検証です。 下層ページは第2期にまわせます。公開後の追加でも単価はほぼ変わりません。 写真は既存素材の流用でも成立します。撮影は必要な箇所を絞ってご提案します。 構成設計と表示・フォームの検証を削ると、公開後の修正費が上回ることが多いです。"
}
},
{
"@type": "Question",
"name": "いつ公開できますか。逆算して発注日を決めたいです",
"acceptedAnswer": {
"@type": "Answer",
"text": "標準的な企業サイトで発注から2〜3か月、原稿がそろっていれば最短6週間です。 内訳は構成設計に2週間、デザインに3週間、実装と検証に3週間が目安です。 止まりやすいのは原稿の確認待ちです。確認の担当と締切を先に決めておくと短縮できます。 公開希望日が決まっている場合は、そこから逆算した工程表をお見積りに添えます。"
}
},
{
"@type": "Question",
"name": "途中でやめることになったら費用はどうなりますか",
"acceptedAnswer": {
"@type": "Answer",
"text": "完了している工程までの実費精算で、未着手分は着手金からご返金します。 工程ごとの金額を契約書に明記するので、途中で止めたときの金額が事前に分かります。 設計書・デザインデータなど、その時点までの成果物はお渡しします。 担当者の異動や方針変更での中断も、追加の違約金はいただきません。"
}
},
{
"@type": "Question",
"name": "公開後、社内で更新できますか",
"acceptedAnswer": {
"@type": "Answer",
"text": "お知らせ・実績・スタッフ紹介は、管理画面から追加と並べ替えまでできます。 レイアウトが崩れない範囲を編集画面側で制限するので、HTMLの知識は要りません。 操作手順書のお渡しと、担当者向けの説明を1時間ほど行います(初回は費用に含みます)。 デザインの変更やページの新設は、こちらで承ります。"
}
},
{
"@type": "Question",
"name": "不具合が出たときはどこへ連絡すればいいですか",
"acceptedAnswer": {
"@type": "Answer",
"text": "制作を担当した者へ直接ご連絡ください。平日は当日中に一次回答します。 メール・電話・チャットのいずれでも受け付けます。窓口は公開後も変わりません。 表示崩れとフォームの不着は、原因の切り分けまで無償で対応します。 サーバー障害など当社以外に原因がある場合も、状況の確認と連絡を代行します。"
}
},
{
"@type": "Question",
"name": "月額の保守費は何に使われますか",
"acceptedAnswer": {
"@type": "Answer",
"text": "更新代行の時間・脆弱性対応・バックアップ・月次の計測レポートの4つです。 更新代行は月2時間まで。超える月は事前にご相談のうえ時間単位で精算します。 土台のソフトウェア更新と表示確認を月1回行い、結果を記録して共有します。 計測レポートには流入元・よく読まれたページ・問い合わせ件数と、次の打ち手を1つ書きます。"
}
}
]
}"use client";
import { useId, useMemo, useState } from "react";
import { Montserrat } from "next/font/google";
const montserrat = Montserrat({
weight: ["500", "600"],
subsets: ["latin"],
display: "swap",
});
/**
* 検索意図別FAQ(構造化データつき)
* 既存のFAQは「単純な開閉リスト」と「カテゴリタブで絞り込む2カラム」で、どちらも質問を
* 隠す/絞る方向の型だった。本ブロックは何も絞らず、質問を検索意図の段階
* (比較検討中→発注直前→契約後)で束ね、回答の結論1文は閉じた状態でも常時見えている。
* 開くのは根拠と前提条件だけで、質問文そのものを検索クエリの形で併記する。
* 同じ配列から FAQPage の JSON-LD を組み、出力するJSONを画面上でも確認できる。
* 飽和idiomは英字キッカーと末尾の「→」リンクを外し、角丸もゼロ。掲載内容はすべて表示サンプル。
*
* 汎用技法メモ(web-design-playbook 還流用):
* - **FAQの構造化データは表示と同じ配列から作る**: 手書きのJSON-LDは文言を直したときに必ず
* 食い違う(Search Console で「マークアップと本文が一致しない」警告になる)。回答テキストは
* 結論1文+根拠の配列を join して1つの acceptedAnswer にまとめ、画面と出力で同じ文字列を使う。
* - **閉じた回答も必ずDOMに置く**: JS で条件レンダリング(開くまで生成しない)にすると、
* クローラと構造化データの対象が消え、ページ内検索にも当たらない。details は閉じている間も
* 中身がDOMに残るので、開閉はネイティブに任せる。
* - **details を一括開閉したいときだけ controlled にする**: open を state で持ち onToggle で
* 同期すると、キーボード操作(Enter/Space)とページ内検索の自動展開を壊さずに
* 「まとめて開く」を足せる。onToggle は開閉後に発火するので event.currentTarget.open を読む。
* - **段階の見出しは sticky にしない**: 縮小プレビューや短い器では sticky がその場で崩れる。
* 段階は左カラムに平置きし、進行は番号と1pxの縦罫で示す。
*/
type Faq = {
id: string;
/** 実際に検索される語。質問文と1対1で持つ */
query: string;
q: string;
/** 回答の先頭1文。結論を言い切る。閉じていても常時表示 */
lead: string;
/** 根拠・前提条件。開いたときだけ見える(DOMには常にある) */
detail: string[];
};
type Stage = {
key: string;
label: string;
/** その段階の読者が何を確かめに来ているか */
intent: string;
faqs: Faq[];
};
const STAGES: Stage[] = [
{
key: "compare",
label: "比較検討中",
intent: "相場を知り、数社を並べて外すところを探している段階。金額と判断基準を先に出す。",
faqs: [
{
id: "budget",
query: "コーポレートサイト 制作費 相場",
q: "制作費はいくらくらいかかりますか",
lead: "企業サイトは80万〜250万円が中心で、金額はページ数と原稿・写真のご用意状況でほぼ決まります。",
detail: [
"10ページ前後・原稿ご支給で80万円台、取材と撮影を含めて20ページ規模になると200万円を超えます。",
"更新のしくみ(管理画面から編集できる範囲)を広げると、ページ数よりも金額が動きます。",
"初回のご相談から概算のご提示までは費用をいただきません。",
],
},
{
id: "choose",
query: "制作会社 比較 選び方",
q: "他社と比べるとき、どこを見れば違いが分かりますか",
lead: "見積書の「デザイン費」の内訳と、公開後1年に含まれる作業範囲を並べると差が出ます。",
detail: [
"デザイン費が1行なら、個別に設計する画面が何枚かを聞いてください。枚数で工数が決まります。",
"公開後の初期不具合対応・軽微な文言修正が有償か無償かは会社ごとに違います。",
"運用を自社で回す前提なら、管理画面の操作手順書が納品物に入っているかを確認してください。",
],
},
{
id: "effect",
query: "ホームページ 作っても 効果ない",
q: "作っても問い合わせが増えないのではと不安です",
lead: "先に「月に何件増えれば成功か」を決め、その数字に効く導線だけを作ります。",
detail: [
"目標件数が決まると、必要なページと不要なページが分かれるので見積も小さくできます。",
"公開後3か月は流入元と離脱箇所を計測し、原稿の差し替えで改善します。",
"検索からの流入が見込めない業種では、広告や紹介からの着地面として設計します。",
],
},
],
},
{
key: "before-order",
label: "発注直前",
intent: "社内を通すために数字と条件を固める段階。見積の読み方と後戻りの条件を隠さない。",
faqs: [
{
id: "quote",
query: "見積書 内訳 削れる項目",
q: "見積の項目が多く、どこを削れるか分かりません",
lead: "削るならページ数と撮影、削らないほうがよいのは設計と公開前の検証です。",
detail: [
"下層ページは第2期にまわせます。公開後の追加でも単価はほぼ変わりません。",
"写真は既存素材の流用でも成立します。撮影は必要な箇所を絞ってご提案します。",
"構成設計と表示・フォームの検証を削ると、公開後の修正費が上回ることが多いです。",
],
},
{
id: "schedule",
query: "ホームページ制作 期間 どのくらい",
q: "いつ公開できますか。逆算して発注日を決めたいです",
lead: "標準的な企業サイトで発注から2〜3か月、原稿がそろっていれば最短6週間です。",
detail: [
"内訳は構成設計に2週間、デザインに3週間、実装と検証に3週間が目安です。",
"止まりやすいのは原稿の確認待ちです。確認の担当と締切を先に決めておくと短縮できます。",
"公開希望日が決まっている場合は、そこから逆算した工程表をお見積りに添えます。",
],
},
{
id: "cancel",
query: "制作 途中解約 費用",
q: "途中でやめることになったら費用はどうなりますか",
lead: "完了している工程までの実費精算で、未着手分は着手金からご返金します。",
detail: [
"工程ごとの金額を契約書に明記するので、途中で止めたときの金額が事前に分かります。",
"設計書・デザインデータなど、その時点までの成果物はお渡しします。",
"担当者の異動や方針変更での中断も、追加の違約金はいただきません。",
],
},
],
},
{
key: "after",
label: "契約後・公開後",
intent: "自社で運用できるかを確かめる段階。連絡先と月額の中身を具体的に書く。",
faqs: [
{
id: "update",
query: "ホームページ 自分で更新 できる",
q: "公開後、社内で更新できますか",
lead: "お知らせ・実績・スタッフ紹介は、管理画面から追加と並べ替えまでできます。",
detail: [
"レイアウトが崩れない範囲を編集画面側で制限するので、HTMLの知識は要りません。",
"操作手順書のお渡しと、担当者向けの説明を1時間ほど行います(初回は費用に含みます)。",
"デザインの変更やページの新設は、こちらで承ります。",
],
},
{
id: "support",
query: "サイト 表示崩れ 問い合わせ先",
q: "不具合が出たときはどこへ連絡すればいいですか",
lead: "制作を担当した者へ直接ご連絡ください。平日は当日中に一次回答します。",
detail: [
"メール・電話・チャットのいずれでも受け付けます。窓口は公開後も変わりません。",
"表示崩れとフォームの不着は、原因の切り分けまで無償で対応します。",
"サーバー障害など当社以外に原因がある場合も、状況の確認と連絡を代行します。",
],
},
{
id: "retainer",
query: "サイト 保守費用 内容",
q: "月額の保守費は何に使われますか",
lead: "更新代行の時間・脆弱性対応・バックアップ・月次の計測レポートの4つです。",
detail: [
"更新代行は月2時間まで。超える月は事前にご相談のうえ時間単位で精算します。",
"土台のソフトウェア更新と表示確認を月1回行い、結果を記録して共有します。",
"計測レポートには流入元・よく読まれたページ・問い合わせ件数と、次の打ち手を1つ書きます。",
],
},
],
},
];
/** 表示と構造化データで同じ文字列を使う(手書きのJSON-LDは必ず本文と食い違う) */
const answerText = (faq: Faq) => [faq.lead, ...faq.detail].join(" ");
function buildFaqPage() {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: STAGES.flatMap((stage) =>
stage.faqs.map((faq) => ({
"@type": "Question",
name: faq.q,
acceptedAnswer: { "@type": "Answer", text: answerText(faq) },
})),
),
};
}
function SearchIcon() {
return (
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth={1.4}
strokeLinecap="round"
aria-hidden="true"
className="mt-px size-3.5 shrink-0 text-gray-400"
>
<circle cx="6.8" cy="6.8" r="4.3" />
<path d="M10 10l3.4 3.4" />
</svg>
);
}
function FaqRow({
faq,
open,
onToggle,
}: {
faq: Faq;
open: boolean;
onToggle: (open: boolean) => void;
}) {
return (
<div className="border-t border-gray-200 py-5">
<p className="flex items-start gap-1.5 text-[11px] leading-relaxed text-gray-500">
<SearchIcon />
<span>{faq.query}</span>
</p>
<h4 className="mt-2 text-[15px] leading-[1.6] font-bold text-gray-900 @2xl:text-base">
{faq.q}
</h4>
{/* 結論1文は閉じていても常時表示。開くのは根拠と前提だけ */}
<p className="mt-2.5 border-l-2 border-amber-600 pl-3 text-[13px] leading-[1.85] font-medium text-gray-800 @2xl:text-sm">
{faq.lead}
</p>
<details
open={open}
onToggle={(event) => onToggle(event.currentTarget.open)}
className="group mt-2.5"
>
<summary className="inline-flex cursor-pointer list-none items-center gap-1.5 py-1 text-[11px] font-semibold text-gray-500 hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 [&::-webkit-details-marker]:hidden">
<svg
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
aria-hidden="true"
className="size-3 text-amber-600 transition-transform duration-200 ease-out group-open:rotate-90 motion-reduce:transition-none"
>
<path d="M4 2l4 4-4 4" />
</svg>
<span className="group-open:hidden">根拠と前提を開く</span>
<span className="hidden group-open:inline">閉じる</span>
</summary>
<ul className="faq-detail mt-1 space-y-1.5 pl-[calc(0.75rem+2px)]">
{faq.detail.map((line) => (
<li
key={line}
className="relative pl-3 text-[12px] leading-[1.9] text-gray-600 before:absolute before:top-[0.85em] before:left-0 before:h-px before:w-1.5 before:bg-gray-300"
>
{line}
</li>
))}
</ul>
</details>
</div>
);
}
export default function IntentFaqStructured({
emitStructuredData = false,
}: {
/**
* true のとき FAQPage の JSON-LD を <script type="application/ld+json"> として出力する。
* 実案件では true。ギャラリーのプレビューは1ページに複数の標本が並び、ページ自身の
* 構造化データと競合するため既定 false(出力されるJSONは画面下部で確認できる)。
*/
emitStructuredData?: boolean;
/** 見出しはこのブロック単体では h2 固定(プレビューの共通propを受けるだけで使わない) */
headingTag?: "h1" | "p";
}) {
const uid = useId();
const [openIds, setOpenIds] = useState<string[]>([]);
const jsonLd = useMemo(() => JSON.stringify(buildFaqPage(), null, 2), []);
const total = STAGES.reduce((sum, stage) => sum + stage.faqs.length, 0);
const setOpen = (id: string, open: boolean) =>
setOpenIds((prev) => (open ? [...new Set([...prev, id])] : prev.filter((x) => x !== id)));
const openStage = (stage: Stage, open: boolean) =>
setOpenIds((prev) => {
const ids = stage.faqs.map((faq) => faq.id);
return open ? [...new Set([...prev, ...ids])] : prev.filter((x) => !ids.includes(x));
});
return (
<section
className="@container w-full max-w-[900px] bg-white text-gray-900"
aria-labelledby={`${uid}-title`}
>
<header className="border-b border-gray-900 pb-5">
<p className="text-[11px] font-semibold text-gray-500">質問と回答</p>
<h2
id={`${uid}-title`}
className="mt-2 text-[22px] leading-[1.45] font-bold tracking-tight @2xl:text-[28px]"
>
検討の段階ごとに、結論から答えます
</h2>
<p className="mt-3 max-w-[56ch] text-[13px] leading-[1.9] text-gray-600">
いただく質問を、比較検討中・発注直前・契約後の3段階に分けました。回答の1行目が結論で、その根拠と前提は必要なときだけ開けます。全
<span className={`${montserrat.className} px-0.5 tabular-nums`}>{total}</span>
問。
</p>
</header>
{STAGES.map((stage, stageIndex) => {
const openCount = stage.faqs.filter((faq) => openIds.includes(faq.id)).length;
const allOpen = openCount === stage.faqs.length;
return (
<div
key={stage.key}
className="grid gap-x-10 border-b border-gray-200 py-7 @3xl:grid-cols-[228px_1fr]"
>
<div className="flex gap-4">
<div className="flex flex-col items-center pt-1">
<span
className={`${montserrat.className} text-[11px] leading-none font-semibold tabular-nums text-amber-600`}
>
{String(stageIndex + 1).padStart(2, "0")}
</span>
<span className="mt-2 w-px flex-1 bg-gray-200" aria-hidden="true" />
</div>
<div className="min-w-0 flex-1 pb-4 @3xl:pb-0">
<h3 className="text-sm font-bold tracking-tight @2xl:text-[15px]">
{stage.label}
</h3>
<p className="mt-2 text-[12px] leading-[1.85] text-gray-500">{stage.intent}</p>
<button
type="button"
onClick={() => openStage(stage, !allOpen)}
aria-expanded={allOpen}
className="mt-3 inline-flex items-center gap-1.5 border border-gray-300 px-2.5 py-1.5 text-[11px] font-semibold text-gray-600 transition-colors hover:border-amber-600 hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none"
>
<span className="whitespace-nowrap">
{allOpen ? "まとめて閉じる" : "根拠をまとめて開く"}
</span>
<span className={`${montserrat.className} tabular-nums text-gray-400`}>
{openCount}/{stage.faqs.length}
</span>
</button>
</div>
</div>
<div className="min-w-0">
{stage.faqs.map((faq) => (
<FaqRow
key={faq.id}
faq={faq}
open={openIds.includes(faq.id)}
onToggle={(open) => setOpen(faq.id, open)}
/>
))}
</div>
</div>
);
})}
<details className="group mt-6">
<summary className="inline-flex cursor-pointer list-none items-center gap-2 py-1 text-[11px] font-semibold text-gray-600 hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 [&::-webkit-details-marker]:hidden">
<span className="inline-flex size-4 items-center justify-center border border-gray-300 text-amber-600 transition-transform duration-200 ease-out group-open:rotate-45 motion-reduce:transition-none">
<svg
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth={1.4}
strokeLinecap="round"
aria-hidden="true"
className="size-2.5"
>
<path d="M6 1.5v9M1.5 6h9" />
</svg>
</span>
このFAQが出力する構造化データ(FAQPage)
</summary>
<div className="mt-3 border-t border-gray-200 pt-3">
<p className="text-[12px] leading-[1.9] text-gray-600">
画面に出ている質問と回答から組み立てたJSON-LDです。回答は結論1文と根拠を連結した1つの
acceptedAnswer になります。本文と同じ配列から作るので、文言を直すと出力も一緒に変わります。
</p>
<pre className="mt-3 max-h-64 overflow-auto border border-gray-200 bg-gray-50 p-3 text-[10px] leading-[1.7] text-gray-700">
<code>{jsonLd}</code>
</pre>
</div>
</details>
{/* 実案件では emitStructuredData を true にして出力する */}
{emitStructuredData ? (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLd }} />
) : null}
<style>{`
@keyframes faq-detail-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.faq-detail { animation: faq-detail-in 240ms ease-out both; }
@media (prefers-reduced-motion: reduce) {
.faq-detail { animation: none !important; }
}
`}</style>
</section>
);
}
shadcn CLI でプロジェクトに追加
npx shadcn@latest add https://designs.first-ch.com/r/intent-faq-structured.json