旧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関数 */],
},
]
このダミーでsingleSectionModeとnoChapterColの2つのフラグが立ち、左カラムが消えて中央1カラムのレイアウトに切り替わる。既存コンポーネントの分岐を活かして、新しいコンポーネントを作らずに済んだ。
(.excel-wide) で動的に幅を切り替えるCSSハック
簿記の本文は文章主体なので、ScrollArticleのmax-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.queryのci(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の作用範囲を読み切る難しさを久しぶりに思い出した一件だった。