開発eurekapu-nuxt4

旧eurekapu.com(WordPress)に置いてあった「Excel講座 関数の活用」のページを、新しいNuxt4側に持ってくる作業をした。ユーザーから「leveraging-functions/index.vue を新しい方に再現して」と具体的なファイルパスで依頼が来たところからスタートした記事。

統合インデックスとMillerスライド形式の2つのURLを切り分け、共通レイアウトにダミーチャプターを食わせてsingleSectionModeを発動させ、最後にfxアイコンが画面に巨大化する事故と、SEOタイトルが空っぽになる事故を踏み抜いた。

URL設計をまず決めた

旧サイトでは1つのURLに「索引+本文+スライド」を全部詰め込んでいた。新Nuxt4側では責務を2つに分けることにした。

  • /lessons/excel/functions … 統合インデックス(11関数の概要+各詳細ページへのリンク集)
  • /lessons/excel/functions/practice … Millerコラムのスライド形式(左に目次、右にスライド本体)

統合インデックスは「全関数を縦に1ページで通読できる」ことを優先し、スライド形式は「1枚ずつ切り替えながら集中して読む」ことを優先する。同じコンテンツでも閲覧モードを2つ用意した。

既存のScrollArticleにダミーチャプターを食わせる

新Nuxt4側には、簿記講座で使っているBookkeepingScrollArticleが既にある。これはscrollChaptersという配列を受け取って、左に章リスト・右に本文を出すコンポーネント。

統合インデックスでも同じ見た目を使いたいが、Excel関数の章立ては「11関数のフラットなリスト」であって、簿記のような階層構造を持たない。そのまま渡すと左の章リストが過剰になる。

解決策は、ダミーのscrollChapters配列を1要素だけ作って渡すこと。

const scrollChapters = [
  {
    id: 'excel-functions',
    title: 'Excel関数の活用',
    sections: [/* 11関数 */],
  },
]

このダミーでsingleSectionModenoChapterColの2つのフラグが立ち、左カラムが消えて中央1カラムのレイアウトに切り替わる。既存コンポーネントの分岐を活かして、新しいコンポーネントを作らずに済んだ。

(.excel-wide) で動的に幅を切り替えるCSSハック

簿記の本文は文章主体なので、ScrollArticlemax-widthは読みやすさ優先で狭めに設定してある。だが、Excel関数の解説は数式モック(商品マスタ+作業シート+数式吹き出し)が横に広い。狭いコンテナだと表が縮んで数式バーがはみ出す。

ここで:has()セレクタを使った。

.scroll-article:has(.excel-wide) {
  max-width: 1200px;
}

Excelモックの最上位要素に.excel-wideクラスを振っておけば、その子孫を持つ.scroll-articleだけが幅広レイアウトに切り替わる。簿記側のコンテンツは.excel-wideを含まないので影響を受けない。

:has()を使わずに親側にクラスを振る場合、ページ側で「Excelコンテンツがあるかどうか」を判定してpropsで渡す必要が出てくる。:has()なら子要素の存在だけで親の幅が変わるので、配線が1段階減った。

fxアイコンが画面いっぱいに巨大化する事故

レイアウトが固まって表示確認していたら、関数名の左に置いてある小さなfxアイコン(SVG)が、突然横幅100%に膨れ上がっていた。1関数ずつ並べているはずの索引が、巨大なfxマークで埋め尽くされて画面が縦に伸びている。

最初は「アイコンのwidthを指定し忘れた」と思って探したが、SVGにはwidth="20"と書いてある。にもかかわらず、ブラウザの開発者ツールで見るとwidth: 100%が当たっていた。

原因を辿ったら、ScrollArticleの中に書いた:deep(svg) { width: 100% }に行き着いた。これは数式モック内の大きな図解SVG(商品マスタの矢印図など)を親幅に合わせて拡大するために入れたCSS。:deep()はScoped CSSの境界を貫通するセレクタで、子コンポーネントのsvgタグまで全部巻き込んでいた。

具体的には、SyntaxCardコンポーネントの中に置いた20px四方の小さなfxアイコンSVGまで、親の:deep(svg)に一網打尽にされてwidth: 100%が当たっていた。

:deep()は強力すぎて、書いた本人の想定範囲を超えて子孫に届く。「特定の図解SVGだけに当てたかった」という意図と、実際にCSSが効く範囲がずれていた。

修正: SyntaxCard内のSVGにインラインstyleで強制サイズ固定

CSSセレクタ側で逃げる方法(クラスを足して:deep(.diagram-svg)に絞る等)も検討したが、SyntaxCardは他のページでも使われているコンポーネントなので、CSS側を弄ると影響範囲が読めなくなる。

最終的に、SyntaxCard内のfxアイコンSVGに直接style="width: 20px; height: 20px; flex-shrink: 0"をインラインで書いた。インラインstyleは:deep()より優先度が高いので、親のCSSに巻き込まれずに固定サイズを守れる。

:deep()を使うときは「SVGみたいなありふれたタグを無修飾で指定すると、子コンポーネント全部に届く」という教訓として残した。

11関数にExcel風モック例題を追加

URL設計と幅とアイコン事故が片付いたところで、11関数(VLOOKUP / IF / IFERROR / INDEX-MATCH / SUM / SUMIF / SUMIFS / TEXT / ROUND系 / INDIRECT / 絶対参照)それぞれに、Excel風HTMLモックの例題を差し込んだ。

モック自体は別記事のexcel-html-mockスキルで作ったヘルパーを使い回している。1関数あたり「商品マスタ+作業シート+数式吹き出し(F2編集モード風の色対応)」のセットを1つ置く形に統一した。

SUMIF→SUMIFS置換のメリットを記事に反映

SUMIFのページを書いていて、引数順序の差から「条件が1つでも常にSUMIFSを使う」べき理由が腑に落ちた。

SUMIFS(集計範囲, 条件範囲1, 条件1, ...)は集計範囲が第1引数。数式セルでF2を押した直後にCtrl+[を押すと、第1引数(集計対象列)に直接ジャンプして、合計される元の数値列をその場で確認できる。

SUMIF(範囲, 条件, 集計範囲)は集計範囲が第3引数なので、Ctrl+[で飛んでも条件列に着地してしまい、数値検証がワンステップ余計にかかる。

「数値検証の動線が1ステップ短い」という、引数順序の実用上のメリットを各関数ページの該当箇所に追記した。

practiceページのタイトルが「- Eurekapu.com」だけになっていた

ページが揃ったので最後にOGP・SEOチェックをしていたら、/lessons/excel/functions/practice のタイトルが「- Eurekapu.com」だけしかなかった。先頭が完全に空白でハイフンから始まっている。検索結果に出たら何のページか分からない。

原因はpracticeページが、Millerスライド形式で表示する関数を?ci=...&si=...のクエリパラメータで指定する作りになっていたこと。ページコンポーネント側のuseSeoMetaは、表示中のスライドが何かを知らないまま固定タイトル(空文字列)を返していた。

修正は、route.queryci(chapter index)とsi(section index)から、表示中のスライドのタイトルを動的に組み立てる形にした。

const seoTitle = computed(() => {
  const ci = Number(route.query.ci ?? 0)
  const si = Number(route.query.si ?? 0)
  const section = scrollChapters[ci]?.sections[si]
  return section
    ? `${section.title} - Excel関数の活用`
    : 'Excel関数の活用'
})

useSeoMeta({ title: seoTitle })

これで「VLOOKUP関数 - Excel関数の活用 - Eurekapu.com」のように、URLを開いた瞬間からスライドの中身がタイトルに乗るようになった。

内部リンク26件を target="_blank" に一括変更

統合インデックスから各関数の詳細ページへ飛ぶリンクが26件あった。元のWordPress版では同じタブで遷移していたが、Nuxt4側では「索引ページに戻ってきやすい」運用にしたかったので、全リンクをtarget="_blank" rel="noopener"で別タブ開きに変えた。

rel="noopener"を入れないと、開いた先のページからwindow.opener経由で元タブを操作される脆弱性が残る。target="_blank"とセットで必ず付ける。

スライド上部の余白の不揃いはArticleモードとTheaterモードの差だった

practiceページを開くと、スライドによって上部の余白の高さが微妙に違っていた。1スライド目は上が詰まっていて、3スライド目はやけに広い。

調べたら、Nuxt Contentの表示モードが2種類あった。

  • Articleモード … 通常の記事レイアウト。ヘッダー+目次+本文の3段構造で、本文上部に固定マージンが入る
  • Theaterモード … 没入型レイアウト。ヘッダーが消えて本文が画面いっぱいに広がる

スライドによって採用モードが食い違っていたので、上部の余白計算が変わっていた。CSSで両モードのpadding-topを同じ値に統一して、どのスライドを開いても上端が揃うようにした。

税理士・会計士フォロワー視点での応用

この「共通レイアウト(ScrollArticle)にダミーのチャプター配列を食わせて、コンテンツ側の都合で幅やレイアウトを切り替える」構図は、顧問先別レポートテンプレートに共通の体裁を敷きたいときに同じ手で行ける。表が広い顧問先と、文章主体の顧問先を1つの土台で出し分けられる。

今日の構図

人間がやったこと: 旧サイトのファイルパス指定(leveraging-functions/index.vue をここに入れて)、URL設計の意思決定(統合インデックスとpracticeを分ける)、画面で「fxアイコンがデカすぎる」「practiceのタイトルが空っぽ」という違和感を拾ったこと、スライド余白の不揃いを目視で発見したこと。

Claude Codeがやったこと: BookkeepingScrollArticleへのダミーscrollChapters渡し、:has(.excel-wide)での動的幅切り替えCSS、:deep(svg)巻き込みの原因特定とインラインstyleでの逃げ、useSeoMetaの動的タイトル化、26件の内部リンクへのtarget="_blank" rel="noopener"一括付与、ArticleモードとTheaterモードのpadding-top統一。

「画面の違和感を拾う係」と「原因を辿って実装で塞ぐ係」の分担が今日もそのまま回った。:deep()の巻き込み事故は、CSSの作用範囲を読み切る難しさを久しぶりに思い出した一件だった。