AI を活用した Blazor アプリとしての www.carecom.de リローンチ

すでに年季の入った carecom.de を作り直した体験についての短いレポート。移行作業の大部分は — もちろん私の監督下で — Claude Code が実行しました。

Harald Mühlhoff 読了 2 分

10 年弱前、私は一度このサイトを完全にゼロから組み直したことがあります — 当時は React、TypeScript、Redux、そして ASP.NET Core バックエンドで(参照: www.carecom.de の構築)。スタックの構想は 2017 年当時には新鮮でわくわくするものでしたが、Web 界のすべての物事と同じく、目に見えて緑青がついてしまいました。そろそろリローンチの時です — 2026 年の C# と .NET が差し出してくれるすべてを使って。

新しい carecom.de はいま、Blazor Web App として .NET 10 上で動いています。サーバー側のプリレンダリングを行い、意味のある場所(お問い合わせフォーム、クッキー通知、写真スライダー、ミニ Space Invaders)にだけ意図的にインタラクティブなコンポーネントを配置しています。背後にはもはや JavaScript フレームワークも Redux のパイプラインもありません — 代わりに UI コンポーネントに至るまで型安全な C#、そして Bootstrap を使わない自前のコンパクトな CSS があります。

ここまでは前置きです。本当にエキサイティングなのは、が書かれているかではなく、が書いたか、です。Razor コンポーネント、CSS、サービスクラス、設定といったコードのうち 99% を優に超える部分は、Anthropic のエージェント型 CLI ツール Claude Code によるものです。一連のセッションの中で、私は目標と反復を定めました。エージェントが計画し、書き、テストし、修正し、次の提案を返してきました。

進め方

まずは要件のすり合わせから始めました — ホスティングモデル、ローカライゼーション、UI フレームワーク。直後に Claude は古いサイトをページごとにスクレイピングし、テキストを忠実に抽出して、画像をフォルダ構造とともにローカルへ保存しました。数回のイテレーションを経て、視覚的にも内容的にもオリジナルに迫る完全なクローンが生まれ — その後のセッションで独自の改良を獲得していきました。

コンポーネントとレイアウトシェル

アーキテクチャの重点は再利用可能なコンポーネントに置きました。SiteHeaderSiteFooterPageHeaderRowLanguageSwitcherRightSidebarPersonCardBlogTeaserReferenceListQuoteBlockTestimonialPhotoSliderTextCarouselCookieConsentSeoTagsSocialIconsLatestPostsSpaceInvaders — 同じパターンが複数箇所に現れる場所はいたるところで、独立した Razor コンポーネントに集約しました。

すべてのコンテンツページに横断的に作用するのが、名前付きスロットを持つシェル SitePage です。コンテナ手前のフル幅要素のための BeforeMain(ホームのカルーセル、Photography のスライダー)、メインカラムとしての ChildContent、自動サブページメニューのオプション拡張としての Sidebar、そして既定のサイドバーを完全に置き換える Aside(お問い合わせページの 2 枚の PersonCard)。各ページは PageBase を継承し、ここに LangEffective のロジックが集中して保持されるため、ページごとのボイラープレートは完全に節約できます。

多言語化 — 宣言的に

ローカライゼーションは URL プレフィックス(/de/…/en/…)と、SiteHeader 右上の言語スイッチで実現しています。実際の二言語テキスト(同じ DOM 構造、異なる単語)は型安全なレコード L(De, En) として中央集約された SiteContent.cs に置かれ、セクションごとの XML ファイルからソースジェネレーターが生成します。SiteContent.Contact.Phone[Effective] が正しい言語のバリアントを返してくれます — IStringLocalizer の重荷も、.resx の生成も要りません。

マークアップ内の構造的な言語分岐 — Consulting ページの IT コンサルティングブロックのような DE 限定セクション全体、あるいは言語ごとに異なる DOM 形 — のためには、2 つの極小 Razor コンポーネント <De><En> があります。これらは MainLayout のカスケードから [CascadingParameter(Name = "Lang")] 経由で有効な言語値を読み取り、その値は URL パスから直接供給されます。これにより、数十個の @if (Effective == "de") ブロックが、HTML のように読める宣言的なマークアップに置き換わります。

Photography

Photography ページでは、42 枚の写真スライダーがブラウザ幅いっぱいに広がります — ただしオリジナル画像 1920×1280 に合わせた 3:2 の固定アスペクト比と object-fit: contain によって。通常のモニターでは各写真が完璧に収まって表示されます。非常に大きなモニターでは max-height: 70vh が高さを制限し、横の細いレターボックスは、縦方向の切り抜きを避けるための意図的な妥協です。キャプションとナビゲーション矢印はマウスオーバー時にのみ現れ、写真自体が主役になるようにしています。ホバーできないタッチデバイスでは、操作要素は常に表示されたままになります。

ブランドアニメーション: 振り付けされた SVG としての caredef

ファビコンは手作業で設計せず、ワードマークから直接生成しました。Claude はピクセル密度プロファイルに基づいて carecom.png 内の最初の「C」のバウンディングボックスを算出し、支配色(#10AE5B — CARECOM グリーン)をサンプリングし、これをもとに 32×32、64×64、192×192 のファビコンをレンダリングしました。

caredef.svg — CARECOM ブランドの連想イメージ

ブランドページ(CARECOM ブランド)の古い caredef.jpg は、その間にアニメーション SVG へと全面的に置き換えられました。CARECOM® ワードマークは base64 埋め込み PNG として(インストールされているどのフォントもこの文字形を再現できないため、ピクセル単位で正確に)、7 つの衛星概念(CompetenceCommitmentCommunicationComputerCompleteCompetitiveCommon Sense)と発音記号の見出しはオリジナルの位置にある本物の SVG テキスト要素として配置しています。呼び出されるとロゴが弾むように飛び込み(オーバーシュート付きのバネ系イージング)、続いて 7 つの語が 150 ms 刻みで、わずかに外側へずれた位置から所定の位置へと順次フェードイン、最後に発音記号が下から飛んできます。prefers-reduced-motion も尊重されます。

イースターエッグ: 6502 の伝統に連なる Space Invaders

Informatiker / Computer Scientist ページでは、Apple ][ 上の 6502 アセンブラによる私の最初のプロジェクトへのオマージュとして、Claude にミニ Space Invaders の統合を依頼しました。結果は、サイトのカラースキーム(フッターの黒地にアクセントの青いインベーダー、得点に応じて明から暗へ段階付け)で、キャンバスラベルが二言語化された、純粋なブラウザ JS による独自のキャンバスゲームです。

そこへ至る道は 3 つあります。プログラミング言語の列挙テキスト内にある「6502」のアンカー、共有可能な直リンク用の専用ルート /ja/play、そしてどのページからでもゲームへ遷移するクラシックなコナミコマンド(↑ ↑ ↓ ↓ ← → ← → B A)です。

CSS アイソレーション

コンポーネント固有の CSS は徹底的に切り出しています。各 Foo.razor に対応する Foo.razor.css が存在し、Blazor が一意のスコープマーカー [b-xxxxx] を付与します。かつて 860 行あった site.css は 150 行弱まで縮みました — デザイントークン、基本タイポグラフィ、真に横断的なパターンだけ。ルールがコンポーネント境界を越えなければならない数少ない箇所(ネストされた SocialIcons アンカー上での SiteFooter のアンカー色、RightSidebar スロット内の <li> ボーダー、ContactForm 内で InputText がレンダーする入力フィールドの幅指定)では ::deep が仕事をします。

プライバシーとアナリティクス

Google Analytics 4 は本物のオプトインとして組み込まれています。クッキーバナーで「同意する」をクリックしない限り、gtag.js は決して読み込まれず、クッキーも設定されず、Google へのデータ送信もありません。明示的な同意(GDPR 第 6 条 1 項 a)の後にはじめてブートストラップが起動し、GoogleAnalyticsTracker コンポーネントが Blazor の内部ナビゲーションのたびに追加の page_view を送信します — そうしないと SPA のルート変更時に Analytics 上では初回のページビューしか記録されません。プライバシーポリシーはこの仕組みを独立した章で説明し、同意撤回の方法を案内します。SeoTags コンポーネントは各ページのタイトル、ディスクリプション、canonical、OpenGraph と Twitter Card のタグ、そして hreflang オルタネートを設定します。

レンダリングモードと躓きの石

すぐに二度遭遇した、重要な技術的教訓。静的レンダリングされた親からの CascadingValue は、@rendermode InteractiveServer サブツリーへのレンダーモード境界を自動的に越えません。その結果、お問い合わせフォームではインタラクティブな移行後にラベルが突然ドイツ語で現れる(ページは /en/contact 配下にあるにもかかわらず)、ミニ Space Invaders では導入ブロックが頑なに DE バリアントを表示する(インタラクティブコンポーネント内部の <De><En> コンシューマーが既定値しか見えていなかったため)といった事象が起きました。いずれの場合も、すっきりした対処法は同じです。Lang をページの Razor から明示的な [Parameter] として渡し、コンポーネント内部で <CascadingValue Value="@Lang" Name="Lang" IsFixed="true"> によって子要素に再公開する、というものです。

まとめ

かつてなら何週間もの手作業を要したであろう一連の作業 — コンテンツ移行、コンポーネント分割、オリジナルに対するピクセル単位の忠実度、GDPR テキスト、メール構成、写真スライダー、ファビコン、ローカライゼーション、ブランドのアニメーション SVG、3 つの発見ルートを持つミニゲーム、徹底した CSS アイソレーション — が、Claude Code との見通しの良い回数のセッションで生まれました。私の関与は実質的に、目標設定、レビュー、折に触れての進路修正に絞られました。残りは、計画・記述・ビルド・スモークテスト・ドキュメント化まで含めてエージェントが片付けてくれました。

同様のリローンチや、自社内でのエージェント型コーディングツールをご検討中でしたら — どうぞお気軽にご連絡ください

Harald Mühlhoff

エラーが発生しました。 再読み込み 🗙

サーバーへ再接続しています …

再接続に失敗しました — 秒後に再試行します。

再接続に失敗しました。
もう一度お試しいただくか、ページを再読み込みしてください。

サーバーがセッションを一時停止しました。

セッションを再開できませんでした。
もう一度お試しいただくか、ページを再読み込みしてください。