# MH4GF / Hirotaka Miyagi / 宮城広隆 > Building [Liam ERD](https://liambx.com), Software Engineer at ROUTE06, inc. > Tokyo, Japan ## What I can do Web Frontend (React, Next.js) / Backend (Ruby on Rails, Go) / GraphQL / Cloud (AWS, Terraform) ## Work Experience ### [ROUTE06, Inc.](https://route06.co.jp/) - Current - 2022 - Software Engineer, Tech Lead - React, GraphQL, Rails, AWS ### [Bit Journey, Inc.](https://bitjourney.com/) - 2022 - 2021 - Software Engineer(Freelance) - React, GraphQL, Rails ### [DXER, Inc.](https://dxer.co.jp/) - 2022 - Software Engineer(Freelance) - React, Go, GraphQL ### [ResortWorx, Inc.](https://resortworx.jp/) - 2021 - 2020 - Software Engineer(Freelance) - Vue, Rails, AWS ### [Timee, Inc.](https://timee.co.jp/) - 2021 - 2018 - Backend Engineer - Rails, AWS ## Find me on - [X](https://x.com/mh4gf) @MH4GF - [GitHub](https://github.com/MH4GF) @MH4GF - [Zenn](https://zenn.dev/mh4gf) @mh4gf - [sizu.me](https://sizu.me/mh4gf) @mh4gf ## About ### 好む振る舞い この資料は、宮城広隆が「自身が働く上で良しとする振る舞い」を言語化しまとめています。 他の方に強制するものではなく、宮城と働くイメージをしやすくする用途を想定しています。 ### Soft Skills ### プロであれ - エンジニアである前に社会人。約束を守る・時間を守る・気持ちよく仕事をする - 残業をしないしさせない。健康に、サステナブルに成果を出せる方法を考える - プロダクトを良くすることにベストを尽くす - フルサイクルでありたい。「自分の仕事じゃないから」とスルーせず、プログラミング以外も可能な限りなんでもやる ### AI活用 - 時間の100%を重要な業務に当てられるよう、雑務や2次的な業務はAIに任せる ref: [Code like a surgeon](https://www.geoffreylitt.com/2025/10/24/code-like-a-surgeon) - 全ての業務はAI起点で始める。PoC・叩き台の作成、調査、ドキュメント作成など、AIに任せられる領域を増やす - 戦略・背景・意図など全てを1枚のマークダウン文書にまとめ、それを保守し、いつでもAIに相談できる状態にしておく - AIは自身の能力を拡張するためのツールとして認識する。門外漢の領域であっても60点を出せる能力を積極的に活用する。出力を鵜呑みにせず理解を深め、自身の血肉にしてからアウトプットする ### リモートワーク - 情報が然るべき方法で「公開・整理・配信」されている状態を目指す ref: [社内向け「透明ガイド」を公開します| sonopy@Ubie Discovery](https://note.com/sonopy/n/na5cd53e7c204) - フロー情報として作業は GitHub Issue に常に書き連ねる ref: [Working Out Loud](https://blog.studysapuri.jp/entry/2018/11/14/working-out-loud) - ストック情報として知見や共有事項はドキュメント or Custom Linter に昇華する - コンテキストを追えるよう可能な限り参照を張る。「このタスクがなぜ必要になったのかわからない」という状況を作らない - 基本非同期コミュニケーションとし、タイムゾーンを気にせず進められるようにする - チーム API を定義し、自身のチーム・他チームが協調しやすい状況をつくる: [30 分で分かった気になるチームトポロジー | Ryuzee.com](https://slide.meguro.ryuzee.com/slides/109) - 地理的距離が離れているからこその雑談の時間を大事にする ### ミーティング - [Google 流、会議をより効率的にする秘訣とルール](https://note.com/shuheikoyama/n/n69c2c3e123fc) - 可能な限り短時間にする - 情報共有ではなく意思決定の場とする - 24 時間前までに資料を用意し共有、認識合わせが済んだらすぐ本題に入る - タイムマネジメント・議事録・ネクストアクションの徹底 - 顔を出す。所作や表情によって相手に与える情報量を増やす ### コミュニケーション - 結論から話す - 文章は要点だけに絞り平易な言葉で - 前提認識のすり合わせと、事実・主観・主張を明確にする - [配慮はするが、遠慮はしない](https://www.nikkansports.com/baseball/news/201801130000090.html) ### Product Development ### Issue driven - 理想状態をまず定義する。制約やリスクと比較し最善を尽くす - お客様・事業への価値貢献を最優先する ### チーム開発 - HRT(謙虚・尊敬・信頼)を持って働く - 検査・適応・透明性を大事にする ref: [The Scrum Guide](https://scrumguides.org/docs/scrumguide/v1/Scrum-Guide-JA.pdf) - 人ではなく事に向かう 邪推をしない - Fail Fast 早期に失敗できる状態を作る - 集中状態にあるタスクがない場合、PR などのレビューを最優先タスクとする ref: [コードレビューのスピード](http://shuuji3.xyz/eng-practices/review/reviewer/speed.html) - 「引き継ぎ」をなくすように働く 怪我や病気で自身が突然離脱しても業務が回る状態に保つ - 適切なドキュメンテーション、作業や残タスクの可視化 - 声を上げた人のフォロワーになる、リアクションをする ref: [チームで仕事をするなら、リアクションし続けよ|森 一貴(Mori Kazuki)](https://note.com/dutoit6/n/ned66041f43ff) - この資料に記載していることをは自身のポリシーであり、違う意見があることは当たり前である ### Ops - You build it, You run it. 自身が作ったものに対して責任を持ち自身で運用する ref: [A Conversation with Werner Vogels](https://queue.acm.org/detail.cfm?id=1142065) - ランタイムやライブラリのアップデートが苦しくならないように Renovate や Dependabot に投資する - トイルは山積みになり続けることが多いが、レバレッジが効く順序を検討し技術で解決していく ### Engineering ### 開発 - 全ての開発は理想のインターフェースから始める - ロジックはテストを書く、書けないなら書けるように責務を分割する - バグの恒久対応はテストで表現する - コーディングスタイル(インデントなど)のレビューは一切せず、linter や formatter に従う ref: [Why Prettier? · Prettier](https://prettier.io/docs/en/why-prettier) - 人間のレビューを減らすために linter を積極的に整備する - ビッグバンリリースはせずフィーチャートグルを利用する - OSS やコミュニティに積極的に還元する ### 意思決定・技術選定 - ADR(Architecture Decision Record)を積極的に書く - 理論的根拠に対する強固な共通の理解を作るため - できないこと(制約・リスク)を具体的に書き、許容できるかを判断できるようにするため - [チーム開発における技術選定の進め方 - ROUTE06 Tech Blog](https://tech.route06.co.jp/entry/2023/06/07/120217) - 今しなくて良い意思決定は可能な限り遅延させ、情報を集め最適な選択肢を選べるようにする ref: [リーン開発での『決定を遅らせる』がやっと理解できた話 - 無気力生活 (ノ ´ω `)ノ ~゜](https://gdgd-shinoyu.hatenablog.com/entry/2018/12/16/081728) - 社内フレームワークではなく主流な手段で解決し、キャッチアップと採用を容易にする - 人間の可読性よりAIの可読性を重要視する ### Git, Commit, Pull Request - [トランクベース開発](https://cloud.google.com/architecture/devops/devops-tech-trunk-based-development?hl=ja)や[GitHub Flow](https://gist.github.com/Gab-km/3705015)を採用し、1 日に複数回以上デプロイを行う - PR は誰がいつ見ても理解できるような粒度でコンテキストを記載する - 想定される質問に対して全て置き回答してからレビューを依頼する - commit は cherry-pick で価値を生む単位 - フィーチャーブランチに対する Force Push はコミットの整理のために活用するが、レビュー開始後は行わない - コミットメッセージの言語は社内の公用語やプロダクトの利用ユーザーの言語に合わせる - 日本人向けのプロダクトにも関わらず英語で書くことに価値は低いと考える ### SaaS - ビジネスのコアではない領域については、コードを書く前に SaaS での解決を検討する 車輪の再発明はしない - 価値が説明できるのであれば有料でも導入できるよう交渉する ### 取扱説明書 ref: https://github.com/konifar/about-me/blob/main/readme/取扱説明書.md ### 働く時間と場所 - 本業: 平日10:00 ~ 19:00 くらい - 副業: 平日 8:00 ~ 10:00, 休日どちらかの日中 - 予定は Google カレンダーに入れています - 「ブロック」とついた予定は通院などで本当にブロックしていて、返答ができないと思います - 「調整可」とついた予定は MTG を入れても大丈夫です - リビングで働いているため、後ろで家族が通ったり声が入ることもあるかも - 声がノイズに感じた場合はミュートするので遠慮なく教えてください ### コミュニケーション方法 - オープンなコミュニケーションが好き - テキストコミュニケーションを好みます(ルーツとしては古き良きインターネットの文章が好き) - 口頭で話した内容もテキストでログを残しがち - 人ではなく事に向かいたいと考えています 誰かを攻撃したり、揚げ足を取ることはありません - 自身の直感は結構間違っているやんけという感覚があります そのため思考をテキストに書き出したり、チームメンバーに壁打ちさせてもらいながら修正していくことを好みます ### 仕事への根源的なモチベーション - 「課題解決」と「学習とその転用」です 自身ができることを増やしていくことに達成感を感じます - そのためエンジニアという仕事は自身にとって天職だと感じていますが、長い人生色々な仕事を経験してみたいとも思っています ### パフォーマンスややる気が落ちる時 - 並列でやることが増えすぎて、すべてが中途半端になってしまった時 ### リフレッシュ方法 - 散歩やジョギング - 趣味の盆栽のようなプログラムをいじって心を落ち着かせる - ペットのうさぎを愛でる ### 一緒に働く方への要望 - 自分へのフィードバックはなんでも歓迎します!些細なことでも教えてもらえると嬉しいです。 - 間違ったことを言うことがあるので、そこに気づいた時は指摘してもらえるとありがたいです。 - なんでもボールを持ちすぎるところがあるので、少しチームに頼りすぎくらいのスタンスでいます。協力してもらえると嬉しいです。 ### MH4GF / Hirotaka Miyagi / 宮城広隆 | Social | Account | | ------------ | ----------------------------- | | X | https://x.com/MH4GF | | GitHub | https://github.com/MH4GF | | Blog | https://mh4gf.dev/articles | | Speaker Deck | https://speakerdeck.com/mh4gf | | sizu.me | https://sizu.me/mh4gf | ### Summary フロントエンド/バックエンドからインフラ、PdM、マーケティングまでを一気通貫で担うプロダクトエンジニアです。 2018 年にスポットワークサービス「[タイミー](https://corp.timee.co.jp/)」に第一号社員として参画し、機能開発・SRE・新規事業 PjM をリードしました。法務チームと連携して [特許取得の QR コード勤怠システム](https://corp.timee.co.jp/news/detail-364/) を実装し、シリーズ C 調達期には TVCM 流入にも耐えるインフラ基盤を構築しました。 その後フリーランスとして複数スタートアップを支援し、React + GraphQL による UI 刷新、Rails モノリスのモダン化、AWS/Terraform でのインフラ 0→1 を推進。2022 年に [ROUTE06](https://route06.co.jp/) へ加わり、商取引 DX プラットフォームのフロントエンドアーキテクチャをリードしたのち、現在は OSS のビジュアルデベロップメントプラットフォーム「Liam」シリーズをプロダクトマネージャー兼テックリードとして企画・開発しています。第一弾 [Liam ERD](https://liambx.com/) はリリース 3 か月で GitHub で 3,700 スターを獲得。 高速な学習と HRT(謙虚・尊敬・信頼)を軸に、プロダクトとチームの成長を両輪で加速させてきました。 こちらも合わせてご覧ください。 - [好む振る舞い](https://mh4gf.dev/behavior) ### Technical Skills - TypeScript, React, Next.js, Vite, Tailwind CSS, Storybook, ProseMirror, reg-suit, Apollo Client - Ruby, Rails, Rspec, RuboCop, graphql-ruby - Go, Gorm, gplgen - Terraform, AWS Fargate, ALB, RDS, S3, CloudFront, CloudWatch, Route53, VPC, Amplify, Cognito - GitHub Actions, CircleCI, Sentry, Datadog, Redash, Slack, Figma, Notion, Google Workspace, Slack, Zoom, Google Meet - Cursor, Cline, Claude Code, Devin AI ### Public Speaking & Publications | Date | Title | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 2025/05/23 | Valibot Schema Driven UI - ノーコード Web サイトビルダーを実装してみよう! - TSKaigi 2025 | | 2025/03/26 | [チームの性質によって変わる ADR との向き合い方と、生成 AI 時代のこれから - #技術選定\_findy](https://findy-tools.connpass.com/event/347251/) | | 2024/06/12 | [Playwright - 高速なフィードバックにより 0→1 フェーズでも生産性に大きく寄与する E2E テストツール - Findy Tools](https://findy-tools.io/products/playwright/33/109) | | 2023/09/09 | [pnpm workspace 実践ノウハウ - #DevelopersGuild](https://speakerdeck.com/mh4gf/pnpm-workspaceshi-jian-nouhau) | | 2022/10/22 | [sassc-rails を利用している我々は、Sass の@import の非推奨化をどのように乗り越えていくか - Kaigi on Rails 2022](https://kaigionrails.org/2022/talks/mh4gf/) | ### OSS Contribution - biomejs/biome - [docs(website): fix italic text for ja docs by MH4GF · Pull Request #1272 · biomejs/biome](https://github.com/biomejs/biome/pull/1272) - $5 a month Sponsor - vitejs/vite - https://github.com/vitejs/docs-ja/pulls?q=is%3Apr+is%3Aclosed+author%3AMH4GF - $5 a month Sponsor - langgenius/dify - [fix(RetrievalConfig): Fix score threshold assignment for zero value by MH4GF · Pull Request #7865 · langgenius/dify](https://github.com/langgenius/dify/pull/7865) - [fix(RunOnce): change to form submission instead of onKeyDown and onClick by MH4GF · Pull Request #8460 · langgenius/dify](https://github.com/langgenius/dify/pull/8460) - Code-Hex/graphql-codegen-typescript-validation-schema - [feat: minimal implementation to support valibot by MH4GF · Pull Request #667 · Code-Hex/graphql-codegen-typescript-validation-schema](https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema/pull/667) - sverweij/dependency-cruiser - [feature(plugin): add alternative implementation for mermaid.js reporter plugin by MH4GF · Pull Request #599 · sverweij/dependency-cruiser](https://github.com/sverweij/dependency-cruiser/pull/599) - prosemirror - [add serializer option to set custom regexp for escaping by MH4GF · Pull Request #68 · ProseMirror/prosemirror-markdown](https://github.com/ProseMirror/prosemirror-markdown/pull/68) - [declare Builders type for builders() by MH4GF · Pull Request #9 · ProseMirror/prosemirror-test-builder](https://github.com/ProseMirror/prosemirror-test-builder/pull/9) - [Fix types for cellSelection by MH4GF · Pull Request #160 · ProseMirror/prosemirror-tables](https://github.com/ProseMirror/prosemirror-tables/pull/160) - フォーラムでのバグ報告や知見共有 - ガイドを和訳し公開: [https://zenn.dev/mh4gf/articles/d25ef1ff30b5a6](https://zenn.dev/mh4gf/articles/d25ef1ff30b5a6) ### Work Experience ### 株式会社 ROUTE06 ― Liam 事業部 (2024.02–現在) Product Manager / Tech Lead プロダクトビルダー向け OSS ツール群「Liam」を 0→1 で企画。第一弾 **「Liam ERD」** を公開。フルサイクルチーム(Eng 5 / Designer 1)をリード - **主な責務:** - プロダクト企画、ロードマップ策定、アーキテクチャ設計、初期マーケティング戦略立案 - チーム組成・2 名のピープルマネジメント、AI ネイティブな開発プロセス設計 - **技術スタック / キーワード:** TypeScript, Next.js (App Router), React Flow, WebAssembly, Supabase, LangChain, Langfuse, Devin AI, Cline - [Introducing Liam ERD](https://liambx.com/blog/liam-erd-introduction) ### 株式会社タイミー(2022/4~2022/10) 業務委託として開発プラットフォームチームに所属し、Rails/Next.js での GraphQL の導入・初期設計・各種オンボーディングを担当しました。 [Rails+Next.js で GraphQL を導入する時に考えたこと - Timee Product Team Blog](https://tech.timee.co.jp/entry/2022/09/29/110000) ### 株式会社ビットジャーニー(2021/9〜現在) 業務委託として [https://kibe.la/](https://kibe.la/) の開発チームに参画し、フロントエンドをメインにフルサイクルに開発を進行しました。 #### ProseMirror を利用したリッチテキストエディタの開発 - ProseMirror の木構造 ⇄DOM⇄markdown text の相互変換により、リッチテキストエディタで記入したテキストを即座に markdown に変換させる - ユーザーフレンドリーなテーブル記法の入力 UI の実装 - [https://blog.kibe.la/entry/rich-text-editor-beta-table](https://blog.kibe.la/entry/rich-text-editor-beta-table) - 列・行・セルの選択や追加、削除などを直感的に行えるように - DOM を効率的にレンダリングするためのキャッシュアルゴリズムの実装 #### フルサイクルな機能開発 - DB 設計・Rails の MVC 設計・GraphQL インターフェース・React コンポーネント・Redash/GA4 での分析までを一貫して担当 - GitHub Actions の導入と開発プロセス中のトイルの自動化 #### リリース 7 年が経過する Rails + React アプリケーションのモダン化 - Sass の@import 非推奨による sassc-rails から cssbundling-rails への移行 - webpack.config.js の最適化 - Sass → CSS in JS への移行のための技術選定 - jQuery からの安全な脱却ロードマップの策定・進行(進行中)、ES5 を上げていくためのリスク評価、古い polyfill の削除等 --- ### 株式会社リゾートワークス(2020/12〜現在) ワーケーションを通じて働く人の“創造性“を刺激する福利厚生サブスクリプションサービス  [ResortWorx](https://resortworx.jp/)  で、インフラ/バックエンドエンジニアを担当しました。 #### インフラ構築の 0→1 に伴う技術選定と構築 プロダクトローンチ初期に求められる最低限の構成で、ランニングコストの抑制とエンジニアがキャッチアップできるようなドキュメンテーションを心がけました。 - Nuxt.js + Rails API の利用者向け Web アプリケーションと、Rails SSR による管理画面のホスティング - Route53 → ALB → ECS / RDS の一般的な Rails のホスティング - GitHub Actions による CD 環境、 SSM を利用した ssh(rails c 等)実行環境 - CloudWatch によるロギングとアラート、AWS Chatbot による Slack 通知 - S3 + CloudFront による SPA のホスティング、GitHub Actions による CD, workflow dispatch を利用したブランチ指定のデプロイ - のちに Nuxt の SSR も必要になったため Vercel へ移行 - 上記の AWS リソースを terraform で IaC 化、draw.io + VSCode によるアーキテクチャ図の記載、その他オペレーションをドキュメント化 #### チケットベースでのチーム開発 - Rails, Nuxt での機能追加 - CloudWatch Logs Insights で可視化・検索ができるよう Rails のログの JSON 化 #### 企画 LP サイトの構築 - Nuxt.js(composition API), Tailwind CSS, Vercel による 0→1 を一人で担当 - デザイナーと Figma 上で会話しながら背中合わせで進行 - Lighthouse の点数をほぼ 100 点に --- ### 株式会社タイミー(2018/07〜2021/09) サービスローンチの 1 ヶ月前にジョイン、初期は 1 人サーバーサイドとして Rails をメインに、プロダクト/会社の成長とともに幅広い業務を担当しました。 #### PjM/PdM/バックエンドエンジニア (2020/08〜2021/04) ##### 当時の課題 会社の成長に伴いガバナンスを強めていく必要性があり、プロダクト「タイミー」における社内オペレーション改善や、コーポレートエンジニアリングの領域で開発を進行しました。 ##### メンバー構成 プロジェクトの開発メンバーとしては 2 名で、関係部署にヒアリングしながら遊軍のように活動しました。 - 自身: プロジェクトマネジメント/ 要件定義/ Rails や Go での実装 - チームメンバー: 元 iOS エンジニア(Rails はビギナー)/ 要件定義/ Rails や Go での実装 - 関わる部署: 経理財務、 CS ##### やったこと ヒアリングや SaaS の選定、スケジューリングや開発着手、リリースまで一気通貫で担当しました。(NDA レベルの業務が多く少しぼかしています) - CRM ツール HubSpot の API 連携 - 経理・財務業務 SaaS との API 連携 - 内部統制に伴う各種機能開発の進行(職務権限規定に沿った権限管理・与信・反社チェックなど) - 社内業務のオペレーション改善 - Mac にインストールし deamon で常駐起動するソフトウェアを Go で開発 - 主に非機能要件であるリリースパイプラインの設計を担当 死活監視、バイナリのセルフアップデート機構、リトライ制御など - チームメンバーに Rails を指導 コードレビューやキャッチアップのための順序立てたタスク振りなど --- #### 新規事業 PjM/バックエンドエンジニア/フロントエンドエンジニア (2021/04〜2021/08) ##### メンバー構成 - 開発チーム: iOS 担当 1 名、 デザイナー 1 名、 Web フロントエンド&バックエンド API2 名 - 関わった部署: 経営, Sales, CS, 経理 ##### 役割 - プロジェクトマネジメント・技術選定・設計・実装(Rails, Vue.js)を担当 - チームのエンジニアは それぞれ iOS と Rails に強いメンバーとのプロジェクトだったため、それぞれの強みを活かしてもらいつつ取りこぼしがないよう拾う立ち回りに努めた - Biz サイドとの折衝、サポートや経理とのオペレーション構築など - エンジニアリングとしてはコアドメインのモデリングや Stripe を利用した決済周りの設計実装を主に担当した ##### 技術選定・初期設計 - Vue.js/vue-router を使った SPA の実装 - 一般的な Rails の環境構築(annotate/rails-erd/bullet/rspec/rubocop) - 監視/ロギング/CI/CD の導入(Datadog, Sentry, lograge を利用したログの JSON 化、 CircleCI) - 商品情報登録・在庫管理・注文・決済履歴などのドメインの境界を意識したモデリング - Rails プロジェクトでの行動指針決め - 原則テストコードを書く、書けないなら書けるよう責務を分割する - トランザクションやロックなど SQL を適切に書く - シンプル・ミニマルに ##### 決済機能の要件定義・技術選定・Stripe API の実装/運用 [新規事業の決済機能として Stripe を導入する上で考えたこと全て - Timee Product Team Blog](https://tech.timee.co.jp/entry/2020/12/10/131108) ##### スキーマ駆動開発の導入・運用 [Rails + RSpec + OpenAPI3 + Committee でスキーマ駆動開発を運用する Tips - Timee Product Team Blog](https://tech.timee.co.jp/entry/2020/07/05/150312) --- #### SRE (2021/01〜2021/04) ##### 当時の課題 プロダクトの成長は進みテレビ CM を打つことになり、現状のスケールしない EC2 によるインフラでは耐えられないだろう、という課題がありました。ただひたすらこなしていた開発業務についてもメンバーが増えスクラムを回せるようになり、組織化が進んで自身の属人性は剥がれてきていました。運よくシニアレベルの SRE の方が採用できたこともあり、その方と主に SRE チームを立ち上げ AWS のアーキテクチャから作り変えるプロジェクトを始め、キャッチアップしつつインフラの移行を進めていきました。 ##### やったこと(公開情報) [Production で Rails6 マルチ DB 対応を小さく始める](https://speakerdeck.com/mh4gf/productionderails6marutidbdui-ying-woxiao-sakushi-meru) [GitHub - MH4GF/ecr-lifecycle: Delete more than specified number of images, and protect if dependent on ECS task](https://github.com/MH4GF/ecr-lifecycle) [Redash を Fargate, Datadog, Terraform で構築/運用する - Timee Product Team Blog](https://tech.timee.co.jp/entry/2020/04/20/175821) --- #### バックエンドエンジニア (2018/07〜2020/01) サービスローンチの 1 ヶ月前にジョイン、初期は 1 人サーバーサイドとして保守運用を担当 Rails をメインに、プロダクト/会社の成長とともに幅広い業務を担当しました。 ##### メンバー構成 - リリース前: iOS…5 人、 サーバーサイド 4 人 - リリース後〜半年程度: iOS…2 人、 サーバーサイド 1 人、 技術顧問数人 - 徐々にメンバーは増えていきました ##### 役割 スタートアップのシード期なので、通常の機能開発や運用はもちろん、経営・CS・経理・営業チームからの要望対応も全てバックログに載せただひたすらにこなしていました。 ##### やったこと - 0→1 の開発・0→1 後のサービスの保守運用・負債解消 - RSpec, Rubocop, OpenAPI3 の導入 - Ruby/Rails のバージョンアップ業 - API のバージョニング、Serializer のスキーマ分割 - サービス固有の強いドメインを持つ機能の設計、実装 ### 免許・資格 - 普通自動車免許 ## Articles ### 最近のコーディングエージェントに渡すコンテキストの作り方 2025 年はコーディングエージェントを活用した開発が主流になっており、様々なプラクティスも生まれています。完全にエージェントに任せてコードがどのように動作するかに注意を払わない Vibe Coding や、詳細な仕様を作成してからエージェントに任せる Spec-driven Development(SDD)など、多くの手法が提案されています。 私自身も普段は Claude Code, Codex, Devin などを活用し開発を進めており、コーディングエージェントに出す指示をどう調整すると実装品質と開発スピードが上がるかを試行錯誤しています。いわゆるコンテキストエンジニアリングです。かつては Cline の Memory Bank に心酔しましたし、Claude Code の Plan モードや、GitHub が公開している SDD フレームワークである Spec-Kit を試したりもしました。 その上で 2025 年 11 月現在は、既存のフレームワークに乗るのではなく自分に合うオレオレフレームワークを作るのが一番効率が良いと考え毎日調整しながら運用しており、**論点駆動開発**と名付けています。 この記事では私が今運用しているコーディングエージェント向けのコンテキストの作り方を紹介します。 ### 結論 以下のテンプレートの内容を埋めながら実装を進めてもらいます。以上です。 https://github.com/MH4GF/issync/blob/fa3462b9a29ff6b5f3caac7aa9f19a0e2a07f37e/docs/progress-document-template.md?plain=1 - Purpose / Overview - Context & Direction - Validation & Acceptance Criteria - Specification - Open Questions - Discoveries & Insights - Decision Log - Outcomes & Retrospectives - Follow-up Issues - Confidence Assessment 実際に運用している様子は以下の issue で見られます。これは自分用に開発している CLI ツールの issue の例で、Claude Code Action にテンプレートを埋めながら開発を進めてもらっています。 https://github.com/MH4GF/issync/issues/61#issuecomment-3483059695 ...これだけでは説明が足りなさすぎるので、この記事では背景や思想、テンプレートの使い方などを詳しく説明します。 ### 背景 ### 現在の仕様駆動開発の壁 AWS の Kiro や GitHub の Spec Kit に代表されるような仕様駆動開発の手法は最近かなり盛り上がっていますが、以下のような問題点が指摘されています。 - 小さなタスクには不向き。Kiro に小さなバグ修正を依頼した際、[4 つのユーザーストーリーにまとめ、16 の受け入れ基準を設定した](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)という話もあります。 - 開発フローが「コードレビュー」から「ドキュメントレビュー」に代わり、長大なマークダウンを読むことになります。全体を読むのはかなり苦痛で細部の問題に気づきづらく、結局コーディングエージェントとの認識齟齬が生まれます。 - 仕様の作成段階では気づけない問題が多すぎます。実装した結果実現不可能だったことに気づいたり、新たな問題に気づくことが多いです。 ソフトウェアエンジニアリングは、達成したいゴールと現時点の制約を比較し、その矛盾をパズルのように解消し続けるプロセスです。仕様駆動開発はその点では、実装前にすべての論点を解消しようとするアプローチであり、実装を通じて発見される新たな制約や矛盾に対して柔軟に対応しづらい構造になっていると感じます。 ### OpenAI Dev Day で紹介されていた plans.md https://www.youtube.com/watch?v=Gr41tYOzE20&t=1s 2025 年 10 月 6 日に開催された OpenAI Dev Day では、Codex の開発者たちがどのように開発を進めているかを紹介していました。その中で Feler さんは plans.md と呼ばれるファイルに仕様や開発計画を書き出させながら開発し、OpenAI 社内で最長セッション時間・トークン数を達成したとのことでした。 これはとても素晴らしいものでした。私のテンプレートはこの plans.md をベースにしつつ、いくつか自身の好みに合わせて修正したものです。 これらの背景を踏まえ、次のセクションからはテンプレートの設計思想やプロンプトエンジニアリングの試行錯誤について紹介していきます。 ### progress-document-template.md 先ほどのテンプレートを再掲します。私はこれを進捗ドキュメントと呼び、Claude Code に記載させます。 https://github.com/MH4GF/issync/blob/fa3462b9a29ff6b5f3caac7aa9f19a0e2a07f37e/docs/progress-document-template.md?plain=1 - Purpose / Overview - Context & Direction - Validation & Acceptance Criteria - Specification - Open Questions - Discoveries & Insights - Decision Log - Outcomes & Retrospectives - Follow-up Issues - Confidence Assessment 仕様駆動ではなく常に更新し矛盾を解消させ続けるためのドキュメントで、1 つの GitHub issue に対して 1 つの進捗ドキュメントを作ります。テンプレートの各セクションを埋めていくと、最終的に良質なコンテキストとなるようセクションを設計しています。 ここではテンプレートの試行錯誤やプロンプトチューニングの気づきを紹介します。 ### テンプレートもプロンプト テンプレートには多くの HTML コメントを記載しており、その多くは AI 向けた記載方法のプロンプトです。HTML コメントは GitHub などのマークダウンレンダラーでは表示されないので、人間は純粋な進捗だけを読むことができます。 進捗ドキュメントを書くのは 100%AI で、人間は読んでレビューするだけという設計です。 テンプレートにプロンプトを含める方法は Spec Kit のアプローチを参考にしています。 https://github.com/github/spec-kit/blob/e6d6f3cdee99752baee578896797400a72430ec0/templates/tasks-template.md ### Open Questions セクション セクションの中で最も重要視しているのがこの Open Questions セクションです。受け入れ条件を満たすために解決すべき論点とその選択肢を、推奨案と自信度つきで記載してもらいます。人間は基本的にここだけを読んで意思決定を行えば良い設計です。決定した内容で Decision Log と Specification を更新してもらいます。 #### 仕様を出力させない 「仕様」ではなく「論点+選択肢」で出力させると意味のない仕様や過剰な設計が出にくくなったと感じます。仕様駆動の場合、指示していないのに監査ログの機能を入れようとしたり、過剰なエラーハンドリングを厳密に実装しようとしたり。Spec Kit も「シンプルな解決策か」「新しいデザインパターンを導入していないか」というチェックを入れていますが、それでも大きな仕様が出てきやすいです。 #### タスクの抽象度 また、論点を出させることでどの抽象度のタスクでもテンプレートを適用できるようになりました。抽象度の高いタスクは論点が多すぎて仕様として事前に決め切ることは難しいですが、出力内容が論点であれば意思決定を後回しにするなど段階的に仕様を決めていくことができます。 #### 推奨案と自信度 選択肢には推奨案とその自信度を 🟢(高)🟡(中)🔴(低) で出してもらうようにしています。これは Devin のタスク進行を参考にしています。色付けは視覚的にわかりやすいです。 自信度が高く人間としても良さそうな選択肢であればそのまま実装に入ってもらいますが、自信度が低い場合は人間も判断しづらい場合が多いので、以下の 2 パターンのどちらかを指示することが多いです。 - 「一旦推奨案で PoC を実装してみて、自信度を上げるための情報を集めて」 - 「その選択肢で良いか判断するためのサブ Issue を作って」 サブ issue を作ったら、また進捗ドキュメントのテンプレートを埋めて作業を進行してもらいます。解決したら親 issue の Open Questions を解決します。これを再帰的に繰り返すことで、どんな抽象度のタスクであってもとにかく前に進められるようにしています。 ### Discoveries セクション 発見や気づきを記載してもらうセクションです。Open Questions で挙げる問いにはコードベースを調べればわかることは記載して欲しくないのですが、事前にこのセクションに必ず書くようにしてもらってからそれは起きづらくなりました。 これは既存コード調査結果のキャッシュとなるので、後続の実装フェーズで無駄なコード探索が不要になり、コーディングエージェントのターン数が減りやすくなる(はず)です。 また実装中何か問題が起きたり、新たな発見があったら都度書いてもらうようにしています。これは作業完了後の振り返りフェーズで問題の再発防止を考える際の重要な情報にもなります。 ### タスクリストは作らない Spec Kit の tasks.md のようにマークダウンのタスクリストでタスクを整理するのはやめ、完全に GitHub の Sub-issues 機能で管理するようにしました。 タスクリスト 1 行では情報量が少なすぎるのと、進行する上で事前に想定したタスクが大幅に変わることもあり、管理が難しいです。またタスクリストをエージェントに出力させると「lint とテストの実行」なども 1 つのタスクとして扱おうとしがちで、粒度のコントロールも難しいと感じています。 GitHub の Sub-issues だと親・子・孫と再帰的に管理できますし、GitHub API で操作もでき、人間としても UI 上での視認性が高いです。 論点の抽象度が高い場合にサブ issue を作り、そこでまた進捗ドキュメントを作ります。サブ Issue が完了したらその意思決定内容や実装内容で親 Issue の進捗ドキュメントを更新します。これを再帰的に行うことでとにかく前に進んでいきます。 サブ issue を解決するタイミングで新たな論点やタスクが生まれることもあるので、事前にタスクを決め切るのはやめ、着手するタイミングで issue を作って進めるのが良いと考えています。 ### Decision Log セクションと Specification セクション 前述の通り仕様はそこまで重視しておらず、意思決定内容を決定ログとして残すようにしています。実装を進める過程で意思決定内容が変わることもあり、これは ADR(Architecture Decision Record)の考え方に近いです。 ただ、仕様セクションは実装フェーズのコーディングエージェントのターン数が減りやすいのでセクションとしては用意しています。 ### Acceptance Criteria セクション 受け入れ条件を記載してもらいます。人間はそこまで読まず、PR のレビューをエージェントにしてもらう時に「受け入れ条件を満たしているか?」を判定してもらうためにセクションを用意しています。 「どのようにテストするか?」も記載してもらいます。Spec Kit は TDD をかなり強く指示していますが、個人的にはテストが書けない状況でも無理やりテストコードを書こうとしてうまくいかないことが多かった印象です。 既存のテストコードパターンを調査させた上で、自動テストと手動テストのどちらかを判断させるのがちょうど良い塩梅でした。 ### 補助ツール 紹介した progress-document-template を GitHub Issue ごとに埋めていく運用ですが、それを補助するための自分用の CLI や Claude Code Commands も用意しています。 ### issync CLI https://www.npmjs.com/package/@mh4gf/issync テンプレートを特定のファイルパスにコピーし、GitHub Issue のコメントに同期するだけの小さなツールです。 進捗ドキュメントをどこで管理するのかはかなり悩んでいました。コーディングエージェントの Read/Edit ツールは効率的なファイル操作に最適化されており活用したいですが、かといって git 管理下のローカルファイルとして保存するとコミット操作やコンフリクトが気になり、また作業完了後にも残ってしまうため古いドキュメントとして後続のコーディングエージェントの無駄なコンテキストになってしまいます。また Git Worktree による複数セッションやリモートのコーディングエージェントによる同時操作がしづらい問題もあります。 辿り着いた解決策として、gitignore されたディレクトリにマークダウンファイルを置きつつ、GitHub Issue のコメントに同期する方法に落ち着きました。これなら GitHub Issue から取り出せば複数セッションで並列作業が可能ですし、人間は GitHub Issue 上でレンダリングされたテキストを読めば良いです。チームメンバーに意思決定内容を共有するための過去ログとしても使えます。 ### Claude Code Plugin issync を使った色んな作業を定型化し簡単に実行するための Claude Code Commands をいくつか作っています。それらは Plugin として提供しているので、インストールすれば簡単に導入できます。 https://github.com/MH4GF/issync/blob/main/.claude-plugins/issync/README.md いくつか抜粋して紹介します。 - `/issync:understand-progress` ... issue の URL を渡しつつ実行し、進捗ドキュメントを理解してもらうコマンドです。新しい Claude Code セッションを開いたらとりあえず叩いて作業を始める汎用的なコマンド。 - `/issync:plan` ... 進捗ドキュメントを初期作成するコマンドです。issync を使ったテンプレートのコピー、コードベース調査、基本セクション記入、Open Questions の記入、GitHub Issue への同期を行います。 - `/issync:create-sub-issue` ... 新規タスクを GitHub Issue として作成します。GitHub の Sub issues API を使って親 Issue を紐づけます。 - `/issync:complete-sub-issue` ... サブ issue を完了として進捗ドキュメントを更新します。振り返りを行いつつ親の Open Questions を解消したり、新しいタスクの提案をします。 Claude Code のプラグインがとても良く、コマンドの内容を更新して push したらいい感じに利用側に配布してくれます。これが便利で、プロンプトの試行錯誤がしやすいです。実装作業自体は Devin や Codex に任せられるのですが、こういった思考作業は Claude Code から離れられなくなっています。 ### 実際の運用 Issue を作ったら `/plan` を実行し、論点が大きければ `/create-sub-issue` で分割、実装できそうなら Devin や Claude Code Action に任せる、PR がマージできたら `/complete-sub-issue` を実行して親 issue を更新、それを並列でどんどん進めていく運用です。 並列作業可能なタスクが圧倒的に増えていきますが、そうなると人間のコンテキストスイッチがボトルネックになるため、人間のタッチポイントを極限まで減らしていくことが重要になっていくと考えます。そのためには思考作業をとにかく言語化・定型化しエージェントが再現可能な状態にしていく必要があります。 コマンドのプロンプトチューニングの結果 `/plan` と `/complete-sub-issue`は品質が高く求めるものが出てきやすくなってきたので、GitHub Actions で Claude Code Action を利用し Issue 作成時に `/plan`を、Issue 完了時に `/complete-sub-issue`を自動実行しています。タッチポイントが減らせた事例であり、また Claude Code Action を整備しておいたことで GitHub モバイルだけで開発が進められるようにもなりました。 https://github.com/MH4GF/issync/blob/main/.github/workflows/auto-plan.yml https://github.com/MH4GF/issync/blob/main/.github/workflows/auto-complete-sub-issue.yml 意思決定後の実装依頼や、完了後の PR が受け入れ条件の達成チェックも自動化できそうですが、例えば UI の変更などではまだうまく QA がしづらく試行錯誤しています。 ### 人間が創造的なタスクやコア業務に集中できるように 自分のテンプレートと、現時点での Claude Sonnet 4.5 だと、今は以下は人間のタッチポイントになるのかなと考えています。エージェントの提案を受けても良いものの、人間が判断をしなければならない最小限のポイントです。 - Issue や Sub issue の作成判断 - Open Questions の意思決定 - 実装後の最終レビュー その他の「二次的な」作業は可能な限り自動で進められるようにワークフローを整備していきたいと考えていますが、なかなか難しいことも事実です。エージェントが実装で詰まっている箇所をサポートすることはまだまだあり、明瞭なコードベースやアーキテクチャ・情報量の多いログの整備、エージェント自身でフィードバックサイクルを回しやすいテスト基盤など、必要な要素はたくさんあります。 Notion で働いている Geoffrey Litt さんは、[自身のブログで「外科医のようにコードを書く」と表現しています](https://www.geoffreylitt.com/2025/10/24/code-like-a-surgeon)。外科医はマネージャーではなく実際に手術をする人です。ただ、準備や二次的な業務、事務作業など行うサポートチームがいることで、外科医は重要な業務に集中できるのです。 コードベース調査や作業が明確な実装タスクはとにかくエージェントに任せ、課題の特定・アーキテクチャ決定・デザインコンセプトの試行錯誤などの重要な業務に 100%時間を使える状態を目指したいです。 ### おわりに この記事では、2025 年 11 月現在の私のコーディングエージェント向けのコンテキストの作り方と、開発スピードを上げるための試行錯誤を紹介しました。自作のフレームワークですが誰かに使ってもらいたいモチベーションはなく、コンテキストエンジニアリングの事例として参考になれば幸いです。モデルの進化や環境の変化に伴い明日には大きく変わっているかもしれませんが、変化に追従しつつ自分に合ったコンテキストの作り方を模索していくつもりです。 ### 落穂拾い その他本筋ではないものの、最近コーディングエージェントとよくやる行動を紹介します。 - コーディングとレビューはセッションを分ける方が最終的な精度が出やすいです。「実装しながらセルフレビュー」は複数の観点を同時に取り組むことになり期待する結果が出づらいです。コンテキストをクリアした新しいセッションを開始し、フラットな視点からレビューだけしてもらうのをよくやっています。 - それをしやすくするために、PR に `devin-review` というラベルを貼るとレビュー観点に従って検査した後そのまま改修を進める GitHub Actions ワークフローを用意しています。CodeRabbit なども精度は高いレビューが出ますが、人間がレビューする前に実装修正までやって欲しい、となると Devin などのクラウド型のコーディングエージェントは便利です。 - 同様にドキュメントワークもコーディングエージェントは長文を書きがちなので、「とにかく書く」と「情報量を維持し圧縮する」セッションは分けた方が良いです。自分はこんな感じの Claude Code Command を用意しています。 https://github.com/MH4GF/dotfiles/blob/7241a1de5a9cd397096c6889a25ab0cc271cfa55/.claude/commands/compact-docs.md ### コードリーディングツールとしてNeovimを使い始めた ### 課題 - Claude Code + Git Worktree によりターミナルで複数ペインでリポジトリを開く機会が増えた - Claude Code に指示する前のコードリーディングも Cursor でやっていたが、Cursor の起動の遅さが気になってきた - ターミナルから高速にコードリーディングできるツールを探し、Cursor から乗り換えたい ### やりたいこと - ファイル名検索と全文検索 - TypeScript コードの定義ジャンプ - コーディングエージェントに指示するためのファイルの相対パスをコピー - GitHub の Web 版でファイルを開く - Git の差分を見る (定義ジャンプなど上記操作も込み) ### 結論 Neovim を導入することにした。ターミナルから高速に動作し、世の中に情報が多く LLM が簡単にセットアップしてくれる。自身のやりたいことを細かくカスタマイズもでき、Cursor や VSCode で手間だったこともショートカット化できたのはかなり嬉しい。 ちなみに[VSCodeVim](https://github.com/VSCodeVim/Vim)拡張で Cursor 上でも Vim 記法による操作をしており、完全に 0 から Vim に入門したわけではない。ただ jj でノーマルモードに戻る程度のカスタマイズしかしていなかったので、プラグインシステムなどのキャッチアップをすることになった。 ### 設定内容 素朴に init.lua に書いている。全ての設定は Claude Code に書いてもらった。この記事は自分向けの利用方法ドキュメントでもあります。 https://github.com/MH4GF/dotfiles/blob/f57ba55e689759ee4a189930cc2f9aad2f681515/.config/nvim/init.lua ### リーダーキー スペースキーをリーダーキーに設定した。これが一般的らしい。 ```lua title="./.config/nvim/init.lua" vim.g.mapleader = " " vim.g.maplocalleader = "\\" ``` ### ファイル名検索と全文検索 [Telescope](https://github.com/nvim-telescope/telescope.nvim) を導入した。 `.` から始まる隠しファイルも検索できるように設定しつつ、 `.git` や `node_modules` は除外するようにした。 ```lua title="./.config/nvim/init.lua" require("lazy").setup({ { "nvim-telescope/telescope.nvim", dependencies = { "nvim-lua/plenary.nvim" }, config = function() require("telescope").setup({ defaults = { file_ignore_patterns = {"%.git/", "node_modules/", "%.DS_Store"}, }, pickers = { find_files = { hidden = true }, }, }) local builtin = require("telescope.builtin") vim.keymap.set("n", "ff", builtin.find_files, { desc = "Find files" }) vim.keymap.set("n", "fg", builtin.live_grep, { desc = "Live grep" }) vim.keymap.set("n", "fb", builtin.buffers, { desc = "Find buffers" }) end, }, }) ``` ### Telescope を開く | キー | コマンド | 説明 | | ----------- | -------------------- | ------------- | | `ff` | Telescope find_files | ファイル名検索 | | `fg` | Telescope live_grep | ファイル内容を文字列検索 | | `fb` | Telescope buffers | 開いているバッファから検索 | できればキーボードショートカットはVSCodeのCmd+Pを踏襲したかったが、iTerm2 + NeovimだとCmdキーが反応しないとのこと。 ### エイリアスでの起動 ```sh $ nf # nvim + Telescope find_files で起動 $ ng # nvim + Telescope live_grep で起動 ``` ### Telescope 検索画面での操作 | キー | 機能 | | --------------- | ---------------------- | | j / k | 上下移動 | | Ctrl+n / Ctrl+p | 上下移動(別パターン) | | Enter | ファイルを開く | | Ctrl+x | 水平分割で開く | | Ctrl+v | 垂直分割で開く | | Ctrl+t | 新しいタブで開く | | Esc | 検索をキャンセル | ### コーディングエージェントに指示するためのファイルの相対パスをコピー ファイルやコードをコピペして指示することが多かったので、簡単にコピーできるようにしたかった。以下の二つのキーバインドを用意した | キー | モード | 機能 | 説明 | | ----------- | ---------- | ------------------------ | ------------------------------------------------- | | `cp` | ノーマル | Copy relative file path | 現在開いているファイルの相対パスをコピー | | `cc` | ビジュアル | Copy file path with code | ファイルパス + 選択範囲をマークダウン形式でコピー | 選択範囲のコピーは以下のような文字列がクリップボードに保存される。 Cline や Roo Code が提供する `Add to Cline` が使いたかった。今回 Neovim で用意したのでどんなコーディングエージェントにも渡せるようになって便利。 ```` @src/components/Header.tsx ``` function Header() { return Hello World; } ``` ```` ```lua title="./.config/nvim/init.lua" local function extract_real_path(path) -- fugitiveのパスから実際のファイルパスを抽出 if path:match("^fugitive://") then local real_path = vim.fn.FugitiveReal(path) if real_path:match("^fugitive://") then real_path = real_path:match("/%.git/.-//%d+/(.*)$") or real_path end return real_path end return path end local function get_relative_path(path) -- 絶対パスの場合は相対パスに変換 if path:match("^/") then return vim.fn.fnamemodify(path, ":.") end return path end local function get_current_file_path() local path = vim.fn.expand("%") path = extract_real_path(path) return get_relative_path(path) end -- ファイル相対パスをコピー vim.keymap.set("n", "cp", function() local path = get_current_file_path() vim.fn.setreg("+", path) print("Copied: " .. path) end, { desc = "Copy relative file path" }) -- ファイル相対パスとコード選択範囲をコピー vim.keymap.set("v", "cc", function() local path = get_current_file_path() -- 選択範囲のテキストを取得 vim.cmd('normal! "vy') local selected_text = vim.fn.getreg("v") -- フォーマットを作成 local formatted = "@" .. path .. "\n\n```\n" .. selected_text .. "\n```" vim.fn.setreg("+", formatted) print("Copied: @" .. path .. " with selected text") end, { desc = "Copy file path with selected code" }) ``` ### GitHub の Web でファイルを開く ファイルを GitHub のパーマリンクで開いて共有するのも多用していたので、以下の二つのキーバインドを用意した。 | キー | モード | 機能 | 説明 | | ----------- | ---------- | ------------------------- | ------------------------------ | | `gh` | ノーマル | Open file in GitHub | 現在のファイルを GitHub で開く | | `gh` | ビジュアル | Open file with line range | 選択した行範囲を GitHub で開く | `gh browse --commit file.js:123-125` を呼び出す形。Neovim と Lua だと CLI ツールを簡単に呼び出せていいですね。 ```lua title="./.config/nvim/init.lua" -- GitHub でファイルを開く vim.keymap.set("n", "gh", function() local path = get_current_file_path() vim.cmd("!gh browse " .. path .. " --commit") print("Opening: " .. path .. " in GitHub at current commit") end, { desc = "Open file in GitHub at current commit" }) -- GitHub でファイルを行番号付きで開く(ビジュアルモード) vim.keymap.set("v", "gh", function() local path = get_current_file_path() -- 選択範囲を取得(ビジュアルモード中に取得) local start_line = vim.fn.line("v") local end_line = vim.fn.line(".") -- 開始行と終了行を正しい順序にする if start_line > end_line then start_line, end_line = end_line, start_line end -- 行番号が0の場合は現在行を使用 if start_line == 0 then start_line = vim.fn.line(".") end local line_part if start_line == end_line then line_part = ":" .. start_line else line_part = ":" .. start_line .. "-" .. end_line end vim.cmd("!gh browse " .. path .. line_part .. " --commit") if start_line == end_line then print("Opening: " .. path .. " line " .. start_line .. " in GitHub at current commit") else print("Opening: " .. path .. " lines " .. start_line .. "-" .. end_line .. " in GitHub at current commit") end end, { desc = "Open file in GitHub with selected lines at current commit" }) ``` ### LSP の設定 これはまだ使い込みが足りていないので、別途記事にしたい。nvim-lspconfig を使うことになりそうだ。コードリーディング用途なのでオートコンプリートは不要 ### Git の差分を見る [fugitive.vim](https://github.com/tpope/vim-fugitive) が人気とのことで入れてみた。Vim の操作感で移動でき、そのままファイルを開けるのはとても良い。元々 CLI での簡単な差分チェックは [tig](https://github.com/jonas/tig) を使っていたが、うまくいけば完全に乗り換えてもよさそう。 ```lua title="./.config/nvim/init.lua" require("lazy").setup({ { "tpope/vim-fugitive", config = function() vim.keymap.set("n", "gs", ":Git", { desc = "Git status" }) vim.keymap.set("n", "gd", ":Git diff --staged", { desc = "Git diff staged" }) end, }, }) ``` ### fugitive.vim を開く | キー | コマンド | 説明 | | ----------- | ------------------ | ---------------------------------- | | `gs` | :Git | Git status 画面を開く | | `gd` | :Git diff --staged | ステージング済みファイルの差分表示 | |
| | | ### エイリアスでの起動 ```sh $ nvg # nvim + Git で起動 ``` Git status 画面での操作 | キー | 機能 | | ----- | -------------------------------- | | = | カーソル下のファイルの差分を表示 | | s | ファイルをステージング | | u | ファイルをアンステージング | | cc | コミット作成 | | Enter | ファイルを開く | | q | 画面を閉じる | ### 終わりに とりあえず自分が欲しかった機能は用意でき、かつ Cursor ではちょっと面倒だったこともショートカットとして用意できたので満足している。新たに覚えることになったキーボードショートカットの数が多いのが悩みどころだが、少しずつ慣れていきたい。 コードリーディング用途の Neovim ということで、nvim-cmp によるオートコンプリートや copilot.vim による GitHub Copilot のコード補完がなくてもよかったのが面白かった。今は 2 割程度は自分でコードを書く機会があるので Cursor を使っているが、Cursor をアンインストールする未来もあるかもなあ。 --- ### 余談: Neovim を採用する前に検討していた案 最初は rg + peco + bat が良さそうかと考えていた。 ### Cmd+P(ファイル名検索)代替 ```bash # 基本的な使い方 git ls-files | peco # 選択したファイルをbatで表示 git ls-files | peco | xargs bat # 選択したファイルをエディタで開く git ls-files | peco | xargs $EDITOR # エイリアス設定 alias fp='git ls-files | peco | xargs bat' alias fpe='git ls-files | peco | xargs $EDITOR' ``` ### Cmd+Shift+F(全文検索)の peco 版 ```bash # rgの結果をpecoで選択 rg --line-number --no-heading "search_term" | peco # より実用的なfunction rgp() { local selected selected=$(rg --line-number --no-heading --color=never "$1" | peco) if [ -n "$selected" ]; then local file=$(echo "$selected" | cut -d: -f1) local line=$(echo "$selected" | cut -d: -f2) bat --highlight-line "$line" "$file" fi } # 行番号指定でファイルを開く版 rgpe() { local selected selected=$(rg --line-number --no-heading --color=never "$1" | peco) if [ -n "$selected" ]; then local file=$(echo "$selected" | cut -d: -f1) local line=$(echo "$selected" | cut -d: -f2) $EDITOR "+$line" "$file" # vim/nvimの場合 fi } ``` ただ、自分のコードリーディングにおいてはファイルが開けるだけはダメで、定義ジャンプやファイルパスのコピーなどの高度な機能を必要としていた。そのため Neovim に切り替えることにした。 ### Rulens: Automating AI-Friendly Coding Guidelines from Your Linting Rules ### Introduction In today's software development landscape, AI coding assistants like GitHub Copilot, Cursor, and Claude have become invaluable tools. However, ensuring these AI tools adhere to project-specific coding standards remains challenging. Rulens addresses this gap by automatically converting your linting configurations into AI-friendly documentation. ### What is Rulens? Rulens is a CLI tool that extracts and formats your Biome and ESLint rules into comprehensive Markdown documentation. This documentation can then be read by AI coding assistants, helping them understand your project's standards before writing any code. https://www.npmjs.com/package/rulens ```bash npx rulens generate ``` The following markdown will be generated: ```md # Project Lint Rules Reference ### 📋 Document Overview This document contains a comprehensive catalog of linting rules enabled in this project. It is automatically generated by [Rulens](https://github.com/MH4GF/rulens) and provides AI code assistants and developers with detailed information about code style and quality requirements. --- ### 📑 Table of Contents - [Introduction](#introduction) - [AI Usage Guide](#ai-usage-guide) - [Biome Rules](#biome-rules) - [ESLint Rules](#eslint-rules) --- ### 📖 Introduction This document lists all active linting rules configured in the project. Each rule includes: - A link to official documentation - A brief description of what the rule enforces - Severity level (when available) Use this reference to understand the code standards and avoid common issues when writing or reviewing code. --- ### 🤖 AI Usage Guide **For AI Code Assistants**: When generating code for this project, please adhere to the following guidelines: 1. **Scan relevant categories first**: Focus on rules in categories related to the code you're generating. 2. **Respect all rules**: Ensure all generated code follows all linting rules. 3. **Avoid common pitfalls**: Check complexity rules to avoid anti-patterns. When uncertain about specific rules, refer to the rule documentation links provided. --- ### 🔧 Biome Rules Biome enforces modern JavaScript/TypeScript best practices with a focus on correctness, maintainability, and performance. ### accessibility Rules in this category ensure that code is accessible to all users, including those using assistive technologies. | Rule | Description | Options | | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------- | | [`noAccessKey`](https://biomejs.dev/linter/rules/no-access-key) | Enforce that the accessKey attribute is not used on any HTML element. | | | [`noAriaHiddenOnFocusable`](https://biomejs.dev/linter/rules/no-aria-hidden-on-focusable) | Enforce that aria-hidden=“true” is not set on focusable elements. | | | [`noAriaUnsupportedElements`](https://biomejs.dev/linter/rules/no-aria-unsupported-elements) | Enforce that elements that do not support ARIA roles, states, and properties do not have those attributes. | | ### 🔧 ESLint Rules ESLint provides static analysis focused on identifying potential errors and enforcing coding standards. ### @typescript-eslint Rules in this category enforce TypeScript-specific best practices and type safety. | Rule | Description | Options | | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------- | | [`await-thenable`](https://typescript-eslint.io/rules/await-thenable) | Disallow awaiting a value that is not a Thenable | | | [`ban-ts-comment`](https://typescript-eslint.io/rules/ban-ts-comment) | Disallow `@ts-` comments or require descriptions after directives | {"minimumDescriptionLength":10} | | [`consistent-type-imports`](https://typescript-eslint.io/rules/consistent-type-imports) | Enforce consistent usage of type imports | [] | ``` ### The Problem Rulens Solves When working with AI coding assistants, ensuring high-quality code that matches your project standards is crucial. Before Rulens, developers had three main approaches: 1. Manually updating custom AI instructions (like `.cursorrules`) - time-consuming and error-prone 2. Relying on type definitions - helpful but limited for style rules 3. Using linting tools - creates a slow feedback cycle of code generation → linting → correction The ideal workflow would have AI understand your rules before generating code. This is exactly what Rulens enables. ### How Rulens Works The process is remarkably simple: 1. Run `npx rulens generate` in your project root 2. This creates a `docs/lint-rules.md` file containing all linting rules with descriptions 3. Add an instruction to your AI assistant: `IMPORTANT: Read coding guidelines in docs/lint-rules.md before beginning code work` That's it! Your AI assistant now understands your linting rules before writing any code. See also [docs/lint-rules.md](https://github.com/MH4GF/rulens/blob/main/docs/lint-rules.md) and [CLAUDE.md](https://github.com/MH4GF/rulens/blob/main/CLAUDE.md) in the rulens project. ### A Real-World Success Story: Reducing vi.mock Usage My personal experience using Claude 3.7 Sonnet and Claude Code demonstrated Rulens' effectiveness. One specific challenge I faced was with test code generation. Claude would frequently use `vi.mock` extensively in Vitest tests, which reduced function purity and created overly broad test scopes. While I could configure ESLint with [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s `no-restricted-vi-methods` rule to prohibit `vi.mock`, applying this after code generation was problematic. AI assistants struggled to refactor existing test code with deeply embedded mocking patterns. By generating coding guidelines with Rulens and instructing Claude to read them beforehand, the AI almost completely stopped using `vi.mock` in generated test code, instead creating tests with better scoping and higher function purity from the start. ### Future Directions Rulens currently supports Biome and ESLint rules, with plans to expand to other linting tools. Some potential improvements include: 1. Support for additional linting tools (Prettier, Stylelint) 2. Rule prioritization and categorization options 3. Custom templates for different documentation formats 4. Integration with more AI coding platforms ### Conclusion Rulens bridges the gap between your linting configurations and AI coding assistants, ensuring that AI-generated code meets your project's standards from the start. This simple but powerful tool streamlines the development process by teaching your AI assistant about your coding standards upfront, reducing rework and maintaining consistent code quality. Rulens reduces the need to write coding guidelines. And it helps you get production-ready code from your AI assistants on the first try. If you like rulens, I’d love your support—please give our repository a ⭐️ on GitHub! I'm also waiting for issues and pull requests. https://github.com/MH4GF/rulens ### このサイトで llms-full.txt を提供し始めた このサイト([mh4gf.dev](https://mh4gf.dev/))で **llms-full.txt** を新たに提供し始めました。 https://mh4gf.dev/llms-full.txt から直接アクセスできます。 mh4gf.dev は私の個人サイトで、自己紹介や職務経歴書、ブログ記事などをまとめています。 llms-full.txt ではそれらを **一枚のマークダウンテキスト** として出力しています。 本記事では、llms-full.txt とは何か・なぜ llms-full.txt を提供し始めたのか、そして Next.js を使った実装方法について紹介します。 ### llms.txt / llms-full.txt とは何か https://llmstxt.org/ **llms.txt** とは、サイト上の重要なドキュメントや概要をまとめておき、生成 AI(LLM)がそれを読み込むことでサイト全体の情報を把握しやすくするためのファイル形式です。 提案者は [Answer.AI](https://www.answer.ai/) の共同創業者である Jeremy Howard 氏であり、Anthropic や Cursor といった LLM 系ツール、また Zapier などもサポートしています。[llmstxt.site](https://llmstxt.site/) には、サポートしているサイトや事例が一覧で掲載されています。 似たような目的のファイルとして sitemap.xml があります。こちらは検索エンジン向けに「すべてのページの一覧」を提供するファイルですが、llms.txt は **LLM が理解しやすいフォーマット**(マークダウン形式)で書かれ、サイトマップより **自由度が高い** のが特徴です。 たとえば、外部サイトへのリンクを含めたり、ページの目次や概要のような人間の文章を追加しても問題ありません。 提案には llms.txt と llms-full.txt という二つの目的のファイルが含まれており、これらの違いは以下です。 - **llms.txt** … 目次や概要のように、サイト全体を俯瞰できるエッセンスのみをコンパクトにまとめたもの。小さなトークン数でサイトの全体像を LLM に把握させ、ユーザーの質問に最適なドキュメントページの URL をクイックに返却するなどの用途が想定されています。 - **llms-full.txt** … 全ドキュメントや全記事を **ほぼ全文** 入れ込んだ大容量ファイル。すべての情報をコンテキストに入れ、ユーザーの質問に対して参照先の URL を返すのではなく回答を直接返す用途が想定されています。 このサイトの場合、全文を入れ込んでもそこまで大量なトークン数にならず、また「すべてをまとめて一気に LLM に渡したい」というニーズが大きかったので、より大容量の **llms-full.txt** を作っています。 --- ### このサイトで提供し始めた理由は? ### 大規模コンテキストウィンドウ時代の活用 OpenAI の GPT-4o は **128,000 トークン**、o1 は **200,000 トークン** までコンテキストに含められると [ドキュメントで説明](https://platform.openai.com/docs/models#gpt-4o) されています。 私の用意した llms-full.txt は、[OpenAI Tokenizer](https://platform.openai.com/tokenizer) で計測したところ **57,385 トークン / 121,192 文字** でした。(2025/01/04 時点) 丸ごと読み込んでも GPT-4o なら余裕があり、コピー&ペーストし LLM に全文を食わせても余裕を持って会話できます。 ### 業務プロダクトでの活用 もともとは仕事で関わっているプロダクト開発でも **llms-full.txt** が役立つのではないか、という発想がありました。 私は Product Required Document やインセプションデッキなどの情報を LLM に投げ込み、コンテキストを理解してもらった上で開発方針やユーザーインタビュー設計など多くの場面で相談しています (もちろん LLM への情報漏洩に気をつけています)。しかし、 - 毎度必要な情報を手作業でコピペするのは面倒 - ChatGPT の GPTs や Dify などで RAG(Retrieval Augmented Generation)の仕組みを構築しても、情報の更新が大変 …という課題がありました。 そこで **「/llms-full.txt を常に自動生成しておき、必要に応じてまるっとコピペして会話する」** という運用は、かなりシンプルで手軽だと感じました。ユーザーマニュアルや周辺情報を LLM が解釈しやすい状態に保守しておき、常にマークダウン 1 枚にまとめられるようにしておく形です。 /llms-full.txt というパスでパブリックに公開せずとも、CI/CD でチーム向けに生成し手動で注入するコンテキストとして活用することは、本格的な検索システムを導入せずとも疑似的に RAG 的な運用ができ、プロダクト開発の効率化につながるのではないかと考えています。 ### パーソナルなサイトを LLM に読ませる価値 個人サイトであっても、以下のような用途があると考えています。 - レジュメのフィードバックやキャリア相談 - 自分風の記事を生成する補助 ... 自分が過去に書いたブログをすべて読ませて、文体やテーマを踏襲した記事下書きを作成してもらう llms.txt が広く流通すれば、LLM が Web サイトにアクセスする際に優先的に利用されるといった状況も考えられますが、現時点では誰かに活用してもらうというより、自身の情報を整理し自分自身のために活用できそうです。 --- ### 実装方法 続いては実装方法を紹介します。このサイトは Next.js で構築し、ブログ記事をマークダウンテキストで記載、[contentlayer](https://contentlayer.dev/) で読み込んで取得しページを生成しています。 今回は以下の要件があったため、Next.js の API Route を利用して実装しました。 - `/llms-full.txt` といった拡張子つきのパスで提供したい - Content-Type を `text/plain` でレスポンスを返却したい ### サンプルコード 以下は実際の簡易版サンプルです。 ```ts title="/app/llms-full.txt/route.ts" import { NextResponse } from "next/server"; import { allArticles } from "@/.contentlayer/generated"; import { externalArticles } from "../_features/articles/data"; import { compareDesc } from "../_utils"; import { me } from "../_constants"; // 情報を集めてきてマークダウン形式で出力 const content = ` # ${me.name} > ${me.description} ### Work Experience ${me.workExperiences .map((work) => { return ` ### [${work.company.name}](${work.company.url}) - ${work.period} - ${work.company.position.join("\n- ")} `; }) .join("\n")} ### Articles ${allArticles .sort((a, b) => compareDesc(a.publishedAt, b.publishedAt)) .map((doc) => `### ${doc.title}\n${doc.body.raw}\n`) .join("\n\n")} ### External Articles ${externalArticles.map((doc) => `- [${doc.title}](${doc.href})`).join("\n")} `; export function GET() { return new NextResponse(content, { headers: { "Content-Type": "text/plain; charset=utf-8", }, }); } ``` このように、文字列テンプレートの中で **自分のデータ**(自己紹介・経歴・記事など)をマークダウン形式で出力し、`text/plain` で返すだけです。contentlayer 経由で取得しているため、コンテンツが更新されたとしても自動的に反映することができます。 実際のコードは[こちら](https://github.com/MH4GF/mysite/blob/a781b2b1f7c0664194b35a1f65f7051f188bc995/app/llms-full.txt/route.ts)です。 --- ### まとめ - **llms.txt / llms-full.txt** とは、**LLM に対してサイト情報を提供するための新しい標準**として提案されているファイルフォーマット - 大規模なコンテキストウィンドウを持つモデルが使えるようになったため、簡易的な RAG としての利用が便利 - Next.js で提供する場合は API Route で実装する ### 2024年の振り返り こんにちは、宮城です。2024 年も振り返りを残します。年次の振り返りはかれこれ 5 回目となりました。 https://mh4gf.dev/articles/2023-summary https://mh4gf.dev/articles/2022-summary https://note.com/mh4gf/n/nf131e1c3bc7b https://note.com/mh4gf/n/n0831d457d2ee 2024 年はプロダクト立ち上げに注力した年でした。前年と比べ少しポエミィな内容が多めです。 結論としては、**ROUTE06 に入社しもうすぐ 2 年、楽しくやっています!** ### 0→1 の暗中模索と見えてきた光明 ROUTE06 での仕事についてです。昨年はプラットフォームエンジニアリングとして組織内での技術の標準化を目指し活動していましたが、2024 年の初めに立ち上がった新規事業チームへ異動となり、ローコードプラットフォーム領域のプロダクト開発を進めています。テックリードとして開発全般をリードしつつ、2024 年後半からは加えてプロダクトオーナーも拝任し、プロダクト戦略やマーケティングを担当しています。 リサーチによる課題・ニーズの特定や、PoC 実装での実現可能性の検証の日々。複数回ピボットも行い、産みの苦しみを味わった 1 年でした。 チームメンバーにはかなり救われました。不確実で答えがない状況が続く中、互いに支え合い、めげずに熱量高く取り組み続けられるのは今のチームだからこそです。 暗中模索の日々から少しずつ光が見え始め、近日中にはプロダクト第 1 弾を公開できる予定です。OSS にするので、まだ README も何もないですが https://github.com/liam-hq/liam で開発を進めています。 ### Product-Led Growth とマーケティング 今年は初めてプロダクトオーナーロールも担当することになりました。現フェーズでの注力事項は提供価値の確立とその仮説検証・初期ユーザー獲得のためのマーケティングです。 ソフトウェアエンジニアとして生きてきた中で、Product-Led Growth におけるマーケティングは自身にとって大きく考え方がアップデートさせられる経験でした。 提供価値が明確なプロダクトは、マーケティングメッセージが一貫しやすく、様々なマーケティング施策に展開しやすく、ユーザーが他者に紹介しやすいです。「良いものを作る」と「使ってもらう」は両輪であり、相互に影響し合うことを改めて理解しました。 書き出してみると当たり前なことだなあとも思いつつ、今までの自分は無意識のうちに避けていたり、他の方に任せてしまっていた領域でした。2025 年は使ってもらうためのアクションやコミュニケーションに本気で集中する年にしていきたいです。 DHH の [小さなチーム、大きな仕事](https://amzn.asia/d/4rpE7nw) にあった以下の記述もよかったです。 > **マーケティングは部門ではない** > あなたの会社にマーケティング部門はあるだろうか? もしないなら、いいことだ。もしあるのなら、彼らだけがマーケティングの責任を負うべきだとは思わないことだ。経理は部門だが、マーケティングはそうではない。マーケティングは、会社のみんなが行うものである。三六五日、二四時間いつでも。  何かコミュニケーションの手段があるのなら、マーケティングはできる。 > > - 電話に出るときもマーケティングだ。 > - メールを送るときもマーケティングだ。 > - あなたの製品が使われるときもマーケティングだ。 > - ウェブサイトに書き込む言葉もマーケティングだ。 > - もしソフトウェアを作ったら、エラーのメッセージもマーケティングだ。 ### 事業の成長と個人の成長 この年次の振り返りは個人の成長の差分確認としての目的が大きいのですが、「プロダクトを伸ばすために今できる全てをやること」が結局は自身の成長に返ってくるため、最近はキャリアロードマップ的なものを考えなくてもよいかな、と思えてきました。「3 年後どうなっていたいか」と聞かれても、最近は「事業を数段大きくさせていたい」と答えています。 少し過去の話をすると、自分はファーストキャリアでシード期の [タイミー](https://timee.co.jp/) に 1 人目の正社員エンジニアとしてジョインし、シリーズ C あたりで離れました。以下の記事に書いています。(今見るとこそばゆいですね) https://note.com/mh4gf/n/n901dafda3ffa タイミーと出会えたのは本当に幸運で、とんでもない人たちと一つの夢に向かってがむしゃらに取り組んだ日々は自分の中で大きな財産であり、かけがえのないものです。少しでも事業成長に貢献できていれば良いなと思いつつ、今思うと圧倒的なスピードで伸びていく事業とプロダクトに引っ張られて目の前の課題をこなしていただけで、ただその場にいただけの人間だったと振り返っています。 今度は、自分の力で事業とプロダクトを引っ張っていけるかという挑戦だと考えています。かつてお世話になった人たちと同じ振る舞いができるか。今この打席に立てることに感謝しつつ、最高のチームメンバーと共に来年はより強く事業に向き合っていきたい。 ### 2024 年触った技術やライブラリ 毎年の差分を確認するためにその年触った技術を振り返っています。今年は技術的な取り組みは減りましたが、それも確認するために列挙してみます。特に思い入れのあるトピックについては取り上げます。 - フロントエンド: TypeScript, React, Next.js(App Router), Tailwind CSS, CSS Modules, Radix UI, Storybook, Vitest, Playwright, msw, graphql-codgen, urql, Apollo Client, Vite, Biome, pnpm, Valibot, Valtio, Yjs, React Flow, ruby/prism, WebAssembly - バックエンド: Ruby, Rails, graphql-ruby, Sequel(ORM), Airtable - インフラ: AWS ECS, Vercel, Terraform - そのほか: GraphQL, GitHub Actions - 生成 AI 関連: ChatGPT, Claude, Perplexity, v0, GitHub Copilot, Ideogram, Dify フロントエンド・バックエンド・インフラは 2022 年あたりから大きく変わらなくなりました。概観すると React + Rails + GraphQL の Web 系領域で枯れたライブラリを選んでいるといったところでしょうか。 唯一特徴的な点として、生成 AI 系ツールを毎日レートリミット近くまで使い倒しています。これほどまで日常に侵食してきてくるとは昨年には考えていなかったところでした。 ### TypeScript / React 一昨年、昨年と引き続き主戦場となりました。状況に応じて Vite と Next.js を使い分けて利用しています。Vite については、Findy さんにお呼びいただいてオフラインで発表もさせていただきました。 https://speakerdeck.com/mh4gf/impressions-after-6-months-of-using-vite-plus-react-router-e448b113-96f4-479a-adea-7d003e5d3fda 今年は色んな PoC をやっていたのですが、その中でもローコードプラットフォームのプロダクト開発に関連する内容として Valibot を中心とした動的 UI 構築について記事を書きました。 https://tech.route06.co.jp/entry/2024/09/26/122250 Yjs を利用したリアルタイム共同編集についても記事を書きました。この記事は久しぶりに 100 はてブを超え、手応えを感じました。 https://tech.route06.co.jp/entry/2024/07/03/154219 記事にはしていないですが、Ruby3.3 でデフォルトのパーサーとして採用された Prism の WASM 版を Node.js から呼び出し利用する、といったちょっと特殊なことをやったのも面白い体験でした。これは来年どこかのタイミングでチームメンバーが発表したりしてくれるはずです。 ### 生成 AI 関連ツール 2024 年は ChatGPT pro mode を中心とした生成 AI 関連ツールに一通り課金し、自身の出力できる成果の幅が圧倒的に広がった年でした。 プロダクトマネジメントやマーケティングなどの経験が薄い領域にもなんとか取り組めているのは生成 AI のおかげです。仕事中も常に複数のタブで並行して出力をさせたり、プライベートでも散歩をしながらイヤホンマイク越しに ChatGPT と英会話の練習をしてもらったりしています。 生成 AI や AI エージェントが「専門領域のジュニアレベルの人間」の仕事を奪うとよく言われますが、実際に奪うのは「学習速度が早く専門領域の横展開が可能な人間」だと考えています。生成 AI は大半の領域で 6~70 点の答えが出せるため、その出力のリスクを評価できる判断力が求められています。 開発経験のない起業家が Cursor を使ってごりごりコードを書く事例も増えていますし、学習速度の速い若手がシニアレベルの出力を出すことも不可能ではないはずです。 自分はこの状況にはそこまで悲観的ではなく、自分自身・組織・プロダクトとしてこの時代の流れにどう乗りこなせていけるかとポジティブに考えています。仕事が奪われてしまっても別の仕事が残るでしょうし、プログラミングは趣味として続けていくでしょう。来年末にはどういった振り返りをしているかも楽しみです。 ### 記事執筆 2024 年は 24 件の記事を書きました。2023 年は 19 件なので微増しています。 https://mh4gf.dev/contents 2024 年前半は月 4 回の記事公開を目標としていましたが、プロダクトに集中し始めてからは止めてしまいました。来年度も個人名義でのアウトプットは減るかもしれませんが、プロダクト関連の発信ができると良いなと考えています。 ### 2025 年は英語 プロダクトが英語圏向けということもあり、来年は英語に力を入れていきたいです。スピーキングとリスニングが壊滅的なためなんとかしたい。海外のテックカンファレンスに参加し現地のエンジニアと会話できる状態を目標に頑張ります。 2024 年関わった皆さん、改めてありがとうございました。2025 年もどうぞよろしくお願いします! ### デスク裏にApple Magic Keyboardをマウントした デスク裏に Apple Magic Keyboard をマウントした。 普段キーボードは [Corne V4 Chocolate](https://shop.yushakobo.jp/products/9442) を使っており、何を隠そう Magic Keyboard は Touch ID 専用機となっている。 デスク上の目立つところには置きたくなく、とはいえ手の届きやすいところに設置したいということで、デスク裏にマウントすることにした。 ### マウントの背景 Magic Keyboard を Touch ID 専用機としてデスク裏に固定するアイデアは、数年前に Reddit で話題になったこの投稿を見たのがきっかけ。 [Under desk iMac fingerprint scanner! : r/macsetups](https://www.reddit.com/r/macsetups/comments/pmwts8/under_desk_imac_fingerprint_scanner/) 投稿者の方は、 iMac と好みのキーボードを使いながら Touch ID の機能も活用するためにこの方法を採用したとのこと。 自分も Macbook をクラムシェルモードで運用したいため似たような構成となった。 ### マウントの方法 キーボード専用のデスク裏マウント製品などないため、DIY での解決を模索した。ホームセンターでの散策や ChatGPT との相談を経て、最終的に DIY 用のドアハンドルをデスク裏に固定するアイデアにたどり着いた。 [Amazon | Sugarello ドアハンドル 取っ手 ドアノブ 取手 プルハンドル 引き出し 引手 戸棚 キャビネット 引き戸 210mm (シルバー) | ドアノブ・ツマミ](https://amzn.to/4dHiYVN) 特別な器具なしに単純なネジ留めだけで固定できたのでよかった。とりあえずキーボードを上に載せるだけにしている。滑り止めを貼った方が良いかも。 余談だが、こういう時に 3D プリンターがあれば自分の理想の形状のマウントブラケットを用意できるんだろうな〜と 3D プリンターが欲しくなる。 ### 使い心地 作業時の視点がこんな感じ。 ![作業時の視点](/images/mount-magic-keyboard/2.jpg) 自身の身体に近い場所に Touch ID があるので押しやすく、同時に机の上もすっきりできた。今まではデスク上の適当なところに置いていたのが、専用の置き場所ができたことで位置が一定になり、ノールックで認証操作ができるようになったのも嬉しい。 Magic Keyboard は充電が必要だが、隣の Macbook のマウント付近に巻き取り式の Lightning ケーブルを常備しているので、適宜そこから引っ張り出して充電している。 今時 Lightning ケーブルが必要なのは面倒ではあるものの、同じく使用している Magic Trackpad も同様の運用が必要なので仕方ないかなと思っている。 ### DifyでのAPIアクセス実践 - Obsidianから呼び出しURL記事の要約をノートに保存する 最近は LLM を使って色々実験しており、その中でも Dify をよく使っています。今回は Dify を使った小ネタを紹介します。 [Dify](https://dify.ai/jp) は、カスタマイズ可能な AI ワークフロー・チャットボット・エージェントをノーコードで作成できるプラットフォームです。作成したワークフローはブラウザや Chrome 拡張機能、API 経由で実行できます。一方、[Obsidian](https://obsidian.md/) は拡張性の高い markdown ベースのノートアプリケーションとして知られています。 今回はこれらのツールを連携させ、以下の機能を実現しました。 1. Dify を利用して、指定した URL の記事を自動的に要約する 2. Obsidian から Dify の要約機能を API 経由で呼び出し、結果をノートとして保存する 私は Obsidian にさまざまなメモやログを蓄積しており、日々気になった Web 上の記事もメモと共に Obsidian に保存し、ナレッジベースとして活用しています。 1 年ほど前から ChatGPT でも Web 記事の内容を読んで返答できるようになったので、普段でも要約をまとめてもらってノートにコピペしていましたが、その作業を自動化して一発で Obsidian のノートに保存できないかと考えていました。それが Dify と Obsidian の組み合わせで実現できました。 Dify と Obsidian の組み合わせを紹介する記事となるとかなりニッチな領域なんですが、両者とも拡張性が高いため、さまざまな用途に活用できると思います。例えば Dify の API アクセスを活用すると、Dify だけでは機能が足りない時に Zapier から API アクセスする形で補うなども可能です。 以下のセクションでは、Dify でのワークフロー作成から Obsidian での実装まで、具体的な手順を解説していきます。 ### 動作イメージ 実際の動作イメージは以下の通りです: ![](/images/generate-url-summary-with-dify-and-obsidian/3.gif) 1. コマンドパレットから「Templater: Create new note from template」を選択 2. テンプレート「url-summary」を選択 3. 要約・保存したい記事の URL を入力 4. 数秒待つと、要約されたノートが自動的に保存される ノートとして保存した後は、LLM の要約結果を必要に応じて調整できます。私は [Evergreen Notes](https://notes.andymatuschak.org/Evergreen_notes) の思想を取り入れて情報整理をしており、ここで既存の他のノートへのリンクを貼るようにしています。 この仕組みはスマホ版の Obsidian でも問題なく動作するため、出先などでも記事を簡単に保存できるのが便利です。 ### Dify での要約ワークフロー作成 Dify を使って、URL から記事を要約するワークフローを作成します。以下がワークフローの概要です。 ![](/images/generate-url-summary-with-dify-and-obsidian/1.png) このワークフローは以下のノードで構成されています: 1. 開始ノード:URL を入力として受け取ります。 2. JinaReader ツール:入力された URL の内容を読み取ります。 3. JSON Parse ツール:JSON 文字列から任意のフィールドを取り出すツールです。ここでは JinaReader の出力から記事のタイトルを取り出します。 4. LLM ノード(GPT-4o):記事の内容を要約します。要約は日本語で、主な議論点と結論を含む 3 行以内の箇条書きで生成されます。 5. 終了ノード:要約内容(content)とタイトル(title)を出力します。 このワークフローを自分の Dify アカウントで再現したい方は、以下の DSL をコピーの上インポートしてください。
ワークフローの DSL ```yaml app: description: "" icon: 🤖 icon_background: "#FFEAD5" mode: workflow name: 要約 kind: app version: 0.1.1 workflow: conversation_variables: [] environment_variables: [] features: file_upload: image: enabled: false number_limits: 3 transfer_methods: - local_file - remote_url opening_statement: "" retriever_resource: enabled: false sensitive_word_avoidance: enabled: false speech_to_text: enabled: false suggested_questions: [] suggested_questions_after_answer: enabled: false text_to_speech: enabled: false language: "" voice: "" graph: edges: - data: isInIteration: false sourceType: start targetType: tool id: 1722151586902-source-1722300182734-target source: "1722151586902" sourceHandle: source target: "1722300182734" targetHandle: target type: custom zIndex: 0 - data: isInIteration: false sourceType: tool targetType: tool id: 1722300182734-source-1723634891190-target source: "1722300182734" sourceHandle: source target: "1723634891190" targetHandle: target type: custom zIndex: 0 - data: isInIteration: false sourceType: tool targetType: llm id: 1723634891190-source-1722299744292-target source: "1723634891190" sourceHandle: source target: "1722299744292" targetHandle: target type: custom zIndex: 0 - data: isInIteration: false sourceType: llm targetType: end id: 1722299744292-source-1722300260176-target source: "1722299744292" sourceHandle: source target: "1722300260176" targetHandle: target type: custom zIndex: 0 nodes: - data: desc: "" selected: false title: 開始 type: start variables: - label: URL max_length: 256 options: [] required: true type: text-input variable: url height: 90 id: "1722151586902" position: x: 30 y: 349 positionAbsolute: x: 30 y: 349 selected: true sourcePosition: right targetPosition: left type: custom width: 244 - data: context: enabled: true variable_selector: - "1722300182734" - text desc: "" model: completion_params: temperature: 0.7 mode: chat name: gpt-4o-mini provider: openai prompt_template: - id: b2b30596-9568-474b-8c30-75da1cfc104b role: system text: "以下の記事を要約してください。要約は、日本語で、主な議論点と結論を含め、箇条書きで、3行以内でお願いします。 {{#context#}}" selected: false title: Generate summary type: llm variables: [] vision: configs: detail: high enabled: true height: 98 id: "1722299744292" position: x: 942 y: 349 positionAbsolute: x: 942 y: 349 selected: false sourcePosition: right targetPosition: left type: custom width: 244 - data: desc: "" provider_id: jina provider_name: jina provider_type: builtin selected: false title: JinaReader tool_configurations: gather_all_images_at_the_end: 0 gather_all_links_at_the_end: 0 image_caption: 0 max_retries: 3 no_cache: 0 proxy_server: null summary: 0 target_selector: null wait_for_selector: null tool_label: JinaReader tool_name: jina_reader tool_parameters: url: type: mixed value: "{{#1722151586902.url#}}" type: tool height: 298 id: "1722300182734" position: x: 334 y: 349 positionAbsolute: x: 334 y: 349 selected: false sourcePosition: right targetPosition: left type: custom width: 244 - data: desc: "" outputs: - value_selector: - "1722299744292" - text variable: content - value_selector: - "1723634891190" - text variable: title selected: false title: 終了 type: end height: 116 id: "1722300260176" position: x: 1246 y: 349 positionAbsolute: x: 1246 y: 349 selected: false sourcePosition: right targetPosition: left type: custom width: 244 - data: desc: "" provider_id: json_process provider_name: json_process provider_type: builtin selected: false title: Extract title tool_configurations: ensure_ascii: 1 tool_label: JSON Parse tool_name: parse tool_parameters: content: type: mixed value: "{{#1722300182734.text#}}" json_filter: type: mixed value: data.title type: tool height: 90 id: "1723634891190" position: x: 638 y: 349 positionAbsolute: x: 638 y: 349 selected: false sourcePosition: right targetPosition: left type: custom width: 244 viewport: x: 68.00119294975991 y: 34.74871751854829 zoom: 0.6686980554677073 ```
ワークフローを作成したら、API キーを取得します。この API キーは後で Obsidian から要約機能を呼び出す際に使用します。 ![](/images/generate-url-summary-with-dify-and-obsidian/2.png) 次のセクションでは、この Dify ワークフローを Obsidian から呼び出す方法について説明します。 ### Obsidian から API 呼び出しとノート保存の実装 Obsidian から Dify の API を呼び出し、生成された要約をノートとして保存する機能を実装します。この実装には Obsidian の [Templater プラグイン](https://github.com/SilentVoid13/Templater) を使用します。Templater プラグインをインストールし、有効化してください。 ### 1. API 呼び出し用 JavaScript ファイルの作成 Dify の API を呼び出すための JavaScript 関数を記述したファイルを作成します。以下の内容で `getSummaryFromUrl.js` というファイルを作成し、Obsidian の vault の適切な場所(自分は `templater-scripts` フォルダにしています)に保存します。 ```js title="/templater-scripts/getSummaryFromUrl.js" /** * * @param {string} url - The URL of the article to summarize. * @param {function(string): Promise} onTitleUpdate - Callback function to handle title updates as they are streamed. * @param {function(string): void} onContentUpdate - Callback function to handle content updates as they are streamed. */ async function getSummaryFromUrl(url, onTitleUpdate, onContentUpdate) { const response = await fetch("https://api.dify.ai/v1/workflows/run", { method: "POST", headers: { Authorization: "Bearer [DifyのAPIキーを貼り付け]", "Content-Type": "application/json", }, body: JSON.stringify({ inputs: { url: url }, // 「開始」ノードで設定した入力フィールド response_mode: "streaming", // Server-Sent Eventsで返却される user: "obsidian", // APIアクセスしたユーザーを識別するための情報。適当な文字列で大丈夫 }), }); const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); let buffer = ""; let bufferObj; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); for (const line of lines) { // レスポンスには先頭に `data: ` という文字列とJSON文字列が含まれている if (line.startsWith("data: ")) { try { // 不要な文字列を取り除いてパース bufferObj = JSON.parse(line.substring(6)); } catch { continue; } // タイトル抽出のノードが終了したらその時点でタイトルを保存 if ( bufferObj.event === "node_finished" && bufferObj.data.title === "Extract title" && bufferObj.data?.outputs?.text ) { await onTitleUpdate(bufferObj.data.outputs.text); } // LLMのテキストチャンクを都度保存 if (bufferObj.event === "text_chunk" && bufferObj.data?.text) { onContentUpdate(bufferObj.data.text); } } buffer = lines[lines.length - 1]; } } } module.exports = getSummaryFromUrl; ``` `[DifyのAPIキーを貼り付け]` の部分を、先ほど取得した Dify の API キーに置き換えてください。 `response_mode: "streaming"` を指定することで、Server-Sent Events(SSE)形式でレスポンスが返却されます。これにより、要約の生成過程をリアルタイムで取得し、ノートに反映することが可能になります。 今回は 「`text_chunk` イベントであれば要約の生成結果だと見なして保存」としていますが、ワークフローで利用する LLM が複数の場合は困るかもしれません。 `text_chunk` イベントのレスポンスにはどのノードで実行されたかという情報が含まれていないためです。 もし困った場合は無理にリアルタイムで更新せずに、 `node_finished` イベントで最終結果を取り出す形でも実現できます。 ### 2. テンプレートファイルの作成 次に、ノートの作成を行うテンプレートファイルを作成します。以下の内容で `url-summary.md` というファイルを作成し、テンプレートフォルダに保存します。(自分は `templater` フォルダにしています。) ```md title="/templater/url-summary.md" ``` ### 3. Templater の設定 Templater の設定で、テンプレートフォルダと `getSummaryFromUrl.js` を保存したスクリプトフォルダのパスを指定してください。 これで設定は完了です。コマンドパレットを開き「Templater: Create new note from template」を実行すると、記事要約を含めてノートに保存されました! ### おわりに 今回は Dify の API アクセスの実践例として、Obsidian からワークフローを呼び出して結果をノートに保存する方法を紹介しました。 今回はある程度シンプルなワークフローでしたが、カスタマイズすることで単なる記事要約に限らず様々な用途に使えるかと思います。 余談ですが、最近はノーコードで実現可能なことは可能な限りそちらに寄せる方が構築スピードや試行錯誤のしやすさから良いと考え、Dify や Zapier を試している面もあります。しかしプログラミングはどうしても楽しいので、結局この記事の例でもコーディングしてしまいました。うまくバランスを取って両方利活用していきたいですね。 ### Next.js App RouterでPagefindを使うときのあれこれ このブログサイトでサイト内検索機能を追加しました。検索が必要なほど記事数はないので完全に自己満足。 右下の ⌘ ボタンか、Cmd + K キーを押すと検索フォームが表示されるので、テキストを入力し試してみてください。 ![テキストを入力し記事を検索する](/images/pagefind-with-app-router/1.gif) サイト内検索機能は[Pagefind](https://pagefind.app/)を利用して実装しました。静的サイトに特化した全文検索ライブラリで、UI フレームワークに依存せずに利用できます。全文検索にも関わらず転送量をかなり抑えることができるのも大きなメリットです。とても便利ですが、Next.js で利用する際にいくつか考えることがありました。今回はそれらを紹介します! Pagefind とは何か、どのように使うか、についてはインターネット上に多くの記事があるため割愛します。 ### 前提 このサイトは Next.js App Router で構築しています。 ブログ記事はマークダウンファイルでリポジトリ上に配置しており、[contentlayer](https://contentlayer.dev/)で読み込み JSON データに変換した後、[Unified.js](https://unifiedjs.com/), [rehype-react](https://github.com/rehypejs/rehype-react)を利用し React コンポーネントに変換します。 これらの処理は React Server Components で実行され、ビルド時に静的な HTML として出力されます。詳しくはリポジトリをご覧ください。 https://github.com/MH4GF/mysite ### Indexing ### Next.js でのインデックスの作成 Pagefind CLI でビルド時にインデックスの作成を行います。Next.js でのビルド結果は `.next` に配置されるため、そのディレクトリを指定します。具体的なコマンドは以下のようになります: ```json title="package.json" "build:pagefind": "pagefind --site .next --output-path public/search" ``` pagefind の生成ファイルは `public/search` に出力しました。Next.js では public ディレクトリに配置したファイルは静的アセットとしてホスティングしてくれるため、それを利用します。これはクライアントサイドの JavaScript から pagefind を呼び出すために必要です。 Pagefind は静的な HTML に対してインデックスを作成するため、 `next build` の後に CLI を実行する必要があります。以下のように `postbuild` で実行することで、 `pnpm build` を実行するだけで後処理として Pagefind を実行することができます。 ```json "build": "next build", "postbuild": "pnpm run build:pagefind", ``` ### インデックスに独自データを追加する 通常は Pagefind CLI の利用だけで良いのですが、このサイトでは NodeJS API を利用しスクリプトファイルを作って実行する形にしました。 背景として、このサイトは他サイトで公開している記事へのリンク集としている側面もあり、Zenn や所属企業のテックブログで書いた記事なども記事一覧にリンクとして掲載しています: http://mh4gf.dev/articles そのため、外部サイトの記事であってもタイトルを検索できたら良いなと考えました。それは NodeJS API で Pagefind を実行し `addCustomRecord()` を使うと実現できました。 以下のようにスクリプトファイルを用意します: ```ts title="scripts/pagefind.mts" import { createIndex } from "pagefind"; // このサイトで表示している外部記事のデータ import { externalArticles } from "../app/_features/articles/data/externalArticles"; async function main() { const { index } = await createIndex({}); if (index === undefined) { throw new Error("Failed to create index"); } // `--site .next` と同等 await index.addDirectory({ path: ".next", }); // addCustomRecord() を利用し、外部記事のデータをインデックスに追加 for (const article of externalArticles) { await index.addCustomRecord({ url: article.href, content: article.title, meta: { title: article.title, externalLink: "true", }, language: "ja", }); } // `--output-path public/search` と同等 await index.writeFiles({ outputPath: "public/search", }); } main(); ``` externalArticles は、外部記事の情報を持つ以下のような構造のオブジェクトです。それを`index.addCustomRecord()` に渡す形で実現できました。 ```ts export const externalArticles = [ { title: "チュートリアル: Yjs, valtio, React で実現する共同編集アプリケーション", publishedAt: "2024/07/03", href: "https://tech.route06.co.jp/entry/2024/07/03/154219", tags: ["route06-tech-blog"], }, ... ] ``` https://github.com/MH4GF/mysite/blob/467b401e369a74f987fa9c5161f54ec6387e6a06/app/_features/articles/data/externalArticles.ts 今回はこの externalArticles を import するために、スクリプトファイルも TypeScript ファイルとしました。もしこれが JSON の読み込みなどであれば mjs でも良いかと思います。 TypeScript ファイルを実行するために、今回は[tsx ](https://github.com/privatenumber/tsx)を使いました。インストールし依存に追加します: ``` pnpm add -D tsx ``` Pagefind CLI を置き換えます。 ```json title="package.json" "build:pagefind": "pagefind --site .next --output-path public/search" // [!code --] "build:pagefind": "tsx ./scripts/pagefind.mts" // [!code ++] ``` ### Search Pagefind では検索結果を表示する UI コンポーネントが同梱されていますが、このサイトでは React コンポーネントを自前で用意したかったため[search API](https://pagefind.app/docs/api/)を直接使うことにしました。 ### Pagefind の初期化 Pagefind は、生成ファイルに含まれる pagefind.js をブラウザから読み込むことで動作しますが、Next.js のビルド時にはそれらがないため、ビルドの失敗を解決する必要があります。 具体的には以下の記事で紹介されていた、window オブジェクトに Dynamic Import で差し込みつつ `webpackIgnore`コメントを使う方法で解決できました。 https://www.petemillspaugh.com/nextjs-search-with-pagefind ```ts void (async () => { if (typeof window !== "undefined" && typeof window.pagefind === "undefined") { try { window.pagefind = await import( // @ts-expect-error pagefind.js generated after build /* webpackIgnore: true */ "/search/pagefind.js" ); } catch { window.pagefind = { search: () => Promise.resolve({ results: [] }) }; } } })(); ``` https://github.com/MH4GF/mysite/blob/467b401e369a74f987fa9c5161f54ec6387e6a06/app/_features/command/items/SearchGroup.tsx#L34-L47 ### React コンポーネントから Pagefind Search API の読み込み Pagefind の Search API は以下のように利用します。 - `const search = await pagefind.search("static");` で検索を実行 - `const oneResult = await search.results[0].data();` で検索結果を取り出す 2 回非同期関数を実行することになります。React コンポーネントで非同期関数を扱う方法はいくつかありますが、React 19 からは Promise から値を取り出す [use](https://ja.react.dev/reference/react/use) API が提供され、Next.js では既に利用できるようになっています。 今回はこちらを利用してみました。個人のサイトなので新しい API をどしどし使う。 ```tsx // Pagefindの型定義 type Data = { url: string; meta: { title: string; }; }; type Result = { id: string; data: () => Promise; }; type Pagefind = { search: (query: string) => Promise<{ results: Result[] }>; }; declare global { interface Window { pagefind: Pagefind | undefined; } } // キャッシュを有効にしつつ、データを取り出す関数 const getData = cache(async (result: Result) => result.data()); // .next内のhtmlファイル名がPageFindの検索結果として使われてしまうため変換する関数 // /server/app/articles/pagefind-with-app-router.html を // /articles/pagefind-with-app-router にする const formatUrl = (url: string): string => { return url.replace(/\/server\/app\/articles\/(.*)\.html/, "/articles/$1"); }; const SearchResultItem: FC<{ result: Result }> = ({ result }) => { // `use()` でPromiseから値を取り出す const data = use(getData(result)); return {data.meta.title}; }; // PageFindの検索を実行する関数 こちらも `cache()` でラップ const search = cache(async (query: string): Promise => { if (!window.pagefind) { return []; } return (await window.pagefind.search(query)).results; }); const InnerSearchResultItems: FC<{ query: string }> = ({ query }) => { // `use()` でPromiseから値を取り出す const results = use(search(query)); return results.map((result) => ( )); }; export const SearchResultItems = () => { return ( ); }; ``` `use()` で Promise から値を取り出す場合、`cache()` 関数と `Suspense` コンポーネントを利用することになります。 `use()` を使う際、再レンダリングにより複数回のデータフェッチが行われることを回避するために、Promise を読み取る関数はキャッシュされている必要があります。そのために `cache()`を利用します。 Promise が解決していない場合、 `use()` は Promise を throw しコンポーネントをサスペンドさせます。 `Suspense` コンポーネントでラップし待機します。 詳しくは以下の記事が参考になります。 https://azukiazusa.dev/blog/promise-context-value-react-hook https://zenn.dev/uhyo/articles/react-use-rfc --- 上記のコード例では Suspense の fallback を省略していたり、スタイリング([shadcn/ui](https://ui.shadcn.com/)の Command コンポーネントを利用しています)を省略しています。詳しくはソースコードをご覧ください。 https://github.com/MH4GF/mysite/blob/ab0cee1ecc0bff89828fa68261446c752d58228d/app/_features/command/items/SearchGroup.tsx ### 終わり 今回は Next.js で Pagefind を組み込む事例を紹介しました。私自身は他の全文検索ライブラリを使ったことはないのですが、Pagefind は簡単に利用でき、困るところはほぼありませんでした。(余談ですが shadcn/ui の Command コンポーネントの調整の方が大変でした。) 今回は使用しませんでしたが、Pagefind は[検索結果のハイライト](https://pagefind.app/docs/highlighting/)にも対応しているためそちらも気が向いたら試せると良いなと思っています。 終わり! ### 2024年にWebエンジニアがWordpressテーマ開発をした備忘録 Wordpress テーマ開発をすることになり、先日無事リリースを迎えたので備忘録的にツール選定や構成の記録を残しておく。 普段は Web エンジニアをしていて、Wordpress も PHP もほぼ初めてなので最適ではない箇所もあるかも。 ### 方針 - 納品後の追加改修等は別のエンジニアが担当する可能性が高いので、可能な限り Wordpress テーマとして王道な選択肢で学習コストを下げたい。 - 静的解析・テスト・継続的デプロイなど、開発生産性を上げるものについては取り入れたい。 ### スターターテーマ https://underscores.me/ を利用した。2012 年の公開と少々古いが、今でも使われているようで情報量も多い。 [Template Hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/)もあまり理解していない状態だったため、一般的な構成を知りたいと思いベースとなるテーマを探したところ、スターターテーマやブランクテーマといったジャンルがあることを知った。その中の一つとして選んだ。 選んだ感想として、基本的なテンプレート構成や、fuctions.php で書いた方が良い各種設定、[get_template_part()](https://developer.wordpress.org/reference/functions/get_template_part/)によるコンポーネント分割などが学べたのがよかった。Linter や Formatter も同梱されているのもありがたい。 ただ完全にまっさらなテーマというわけではなく、モーダルの開閉やカスタマイザーなどいくつかの JavaScript ファイルがあったが、これらは要件に合わなかったためほとんど 0 から作り直した。Sass のコンパイルも設定されていたが CSS で十分なため外した。 ### カスタム投稿タイプとカスタムタクソノミー 管理画面上で簡単に作成・更新できる[Custom Post Type UI](https://ja.wordpress.org/plugins/custom-post-type-ui/)プラグインを利用した。今回は「カスタム投稿として絞り込んでデータ取得し、専用のテンプレートで表示できれば良い」というシンプルな要件だったため、コードは書かずよく使われているプラグインで実現した。 ### カスタムフィールド [Smart Custom Fields](https://ja.wordpress.org/plugins/smart-custom-fields/)プラグインを利用した。おそらく最も有名なのは[Advanced Custom Fields](https://ja.wordpress.org/plugins/advanced-custom-fields/)プラグインだと思われるが、定義できるデータ型のうち繰り返しフィールド(key/value のオブジェクトの配列を表現できるフィールド)が月額課金が必要な有料プランのため困ってしまい、無料で実現できるプラグインとして選択した。 最小限かつ必要十分な機能を揃えている印象で、とてもよかった。エディタ画面での入力フィールドに説明を置けるのも良い。 ### JavaScript ライブラリ アニメーションやインタラクションは基本的には CSS アニメーションや Web API を使った自前実装で解決できたものの、複雑な領域については JavaScript ライブラリを利用した。 - カルーセル ... [Swiper](https://swiperjs.com/) - SVG アニメーション ... [@lottiefiles/lottie-player](https://www.npmjs.com/package/@lottiefiles/lottie-player) - 慣性スクロール ... [Lenis](https://lenis.darkroom.engineering/) - パララックスアニメーション ... [GSAP](https://gsap.com/) / [ScrollTrigger](https://gsap.com/docs/v3/Plugins/ScrollTrigger) カルーセルは CSS の scroll-snap で、パララックスは CSS の perspective でできるかも...?と思いつつ、今回は時間の都合上 JavaScript ライブラリに任せることにした。いずれ挑戦してみたい。 ### CSS [Tailwind CSS](https://tailwindcss.com/)を利用した。Tailwind CLI を使って `dist/style.css` に出力し、[wp_enqueue_style()](https://developer.wordpress.org/reference/functions/wp_enqueue_style/) で読み込む素朴な運用だ。 ここだけは方針にある王道な選択肢か?と言われたら疑問は残ってしまうが、Scoped CSS が使えない単純な PHP テンプレートという状況で BEM 等の CSS 設計を考えるより保守しやすく、かつ CSS in JS ライブラリと違い JavaScript への依存がないため、PHP でも運用可能と考えた。 この選択については、デザインシステムとしての統一性と拡張性が得られたり、生成 AI のサポートが受けやすいなどの想定していた価値を享受できた部分はあるが、Wordpress のコンポーネント分割の難しさによる相性の悪さも感じた。この件は長くなるため別の記事で紹介したい。 ### Linter, Formatter [PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer), [Biome](https://biomejs.dev/), [Prettier](https://prettier.io/)を利用した。使い分けはこんな感じ。 - PHP ファイルの Lint, Format ... PHP_CodeSniffer - JavaScript ファイルの Lint ... Biome - それ以外の Format ... Prettier 最初は PHP ファイルの Format も、慣れている Prettier でできないかなーと [prettier/plugin-php](https://github.com/prettier/plugin-php)を利用していたが、HTML の一部に PHP を埋め込む書き方の場合 1 トークンごとに改行されるなどで結構見づらかったため、underscores に同梱されていた PHP_CodeSniffer に切り替えた。Linter としても PHP のよろしくない書き方をしている箇所の指摘もしてくれるため PHP の学習に役立った。 Biome は推奨ルールを設定するだけで簡単に使えるので良い。 ### テスト Playwright の [toHaveScreenshot()](https://playwright.dev/docs/test-snapshots) を使った Visual Regression Test(VRT)だけ用意した。PC/モバイル/タブレットサイズのブラウザでアクセスし全ページのスクリーンショットを保存しておき、コードの変更によって出てきた差分が意図的かを確認する。スクリーンショットは Git リポジトリにコミットして、特に VRT のための SaaS などは利用していない。一人開発のため CI も設定せずローカルでのみテスト実行した。 これは最低限の労力で十分な効果を発揮してくれた。例えばレスポンシブ対応のためのスタイル調整が PC 側に影響していないかなどに気付けるのはもちろん、とにかく常に全ページ問題なく開けることが確認できているのは安心感があった。 スクリーンショットの差分は GitHub 上で確認もでき、履歴も見れるので、デザイナーにも GitHub リポジトリにアクセスできるようにしてもらい、開発終盤のデザインレビューで出てきた細かい調整などは差分を見てもらう運用にしていた。 ### ソースコード管理とデプロイ 今回は FTP でデータを VPS にアップロードする形でのデプロイだった。テーマディレクトリを Git リポジトリとして管理し、[SamKirkland/FTP-Deploy-Action](https://github.com/SamKirkland/FTP-Deploy-Action)を使って GitHub Action 経由で FTP を実行するようにした。手動で FTP することがなくなったため安全になり、かつ git push したら数十秒でデプロイが完了するので非常に楽になった。 テーマディレクトリ以外も Git 管理したいものが出てくるかもと思い、リポジトリルートに `wp-content/themes/theme-name` というディレクトリを置いてその中で開発していたが、特に何もなかったので次回は `theme-name` をリポジトリルートにして良いかも。 手元での環境構築には[Local](https://localwp.com/)を利用した。Virtualbox を使って nginx, MySQL, PHP の環境を簡単に作ってくれて、特に困ることもなかった。自己署名による SSL 化もワンクリックでできて助かる。 Local で作った環境のテーマディレクトリは Mac だと `/Users/ユーザー名/Local Sites/サイト名/app/public/wp-content/themes` になり、Git リポジトリは別ディレクトリで作っていたため、シンボリックリンクを貼ることで Local の環境で使えるようにした。 ### Wordpress テーマ開発をした感想 React などの UI ライブラリと比べて、純粋に HTML、CSS、JS を書く経験はとても良かった。今は Internet Explorer が退場し、HTML や CSS だけでできることが増えており、必要に応じて JavaScript を使う程度で済むことが多い。また TypeScript がなくても JSDoc で十分に良い開発体験を得られた。Web の基礎技術の進歩を享受できたと感じる。 WordPress については、CMS として必要な機能がほぼすべて揃っており、ネット上に多くの情報があるため、「PHP、CSS、JavaScript でカスタマイズ可能なローコードツール」としての開発生産性は非常に高いと感じた。 PHP テンプレートについては、リクエストごとに WP_Query でデータを取得し、ループと共に HTML をレンダリングするというシンプルな方法はわかりやすかった。ただロジックとプレゼンテーションの分離が難しく、テンプレートパーツによるファイル分割はできるものの引数を簡単に渡せる程度で、コードの可読性やメンテナンス性は低いと感じた。React などのモダンフレームワークが解決しようとした課題や、SSR や React Server Component などの新しい流れについて改めて理解した。 ### おわり いい経験でした。 ### GitHub Scheduled ReminderによるCI失敗時の通知 GitHub Actions の CI が失敗した時にリアルタイムで通知させたいと思い調べていたところ、そういえば [Scheduled Reminder](https://github.com/settings/reminders) の real-time alerts に `Your PR has failed CI checks` という項目があることを思い出した。 Schedued Reminder についてはこれらの記事を参照ください: https://docs.github.com/ja/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-your-membership-in-organizations/managing-your-scheduled-reminders https://tech.route06.co.jp/entry/2023/04/25/154828 他の通知設定と違いこの項目だけテキスト入力が必要で、ドキュメントを見てもどういった値を入れれば良いかわからず、今まで使わずに放置してしまっていた。 自身の関わるプロダクトでは `frontend-lint` という名前の CI Job があったため試しに入力してみると、確かに失敗時に通知が来てくれた。Require Status Checks の設定と同じなのかも。 Slackの通知画面 カンマ区切りで設定すれば良いということなので、現在はこのように設定している。 Scheduled Reminder の設定画面 'frontend-lint,frontend-test,backend-lint,backend-test' Slack 通知はリアルタイムで見ていることが多いので、その仕組みに乗れたので満足だ。 --- あまりにも `Your PR has failed CI checks` についての情報が少なくないかと調べていると、「設定例が非常に少なくわからない」という Discussions があった。 https://github.com/orgs/community/discussions/23986 Scheduled Reminder の設定値はカンマ区切りの文字列なので、ジョブ名にカンマが含まれているとうまく動作しないとのこと。これは matrix を使うと起きるため困ることは多そう。 また「なぜ全ての CI 失敗時に通知を受け取れないのか?」という意見もあった。これもそう思う、改善に期待したい。 ### ワークチェアを手放し、ステッパーとスツールに移行した ワークチェアを手放し、ステッパーとスツールだけで作業している。 ![Xiserのステッパー](/images/stepper-and-stool/3.webp) ステッパーは見た目通りの器具で、ステップに両足を乗せて右、左と踏み込む運動器具。意外と負荷がある。 買ったのはこれ。(公式サイトが見当たらないので mybest さんのリンク) https://my-best.com/products/309587 結構なお値段だが、安価なステッパーの場合連続使用時間が 1 時間などに決まっていて、超えると壊れてしまうとのこと。ネットの情報ではステッパーを買うなら Xiser 一択とのことなので買ってみた。 ステッパーは自動昇降デスクと組み合わせて使っている。作業中常に踏むわけではなくて、ビルドの待ち時間や思考中などのちょっとした空き時間に踏んでいる。なので作業中の基本姿勢は両足並行の状態でステッパーの上に立つ形。 ステッパーなしの単純なスタンディング作業だと長時間同じ姿勢になるためすぐ疲れてしまうのだが、適宜ステッパーを踏んで体勢を変えることで疲れにくくなり、立ち作業が継続しやすくなった。 スツールは Zara Home のアッシュ バー スツール。Zara Home のインテリアは安価でデザインがとても良くて好き。 [アッシュ バー スツール - ブラウン | ZARA Japan / 日本](https://www.zara.com/jp/ja/%E3%82%A2%E3%83%83%E3%82%B7%E3%83%A5-%E3%83%8F%E3%82%99%E3%83%BC-%E3%82%B9%E3%83%84%E3%83%BC%E3%83%AB-p42365073.html) スツールは補助的な役割で、立つのに疲れたら座る形。ワークチェアほど座り心地はよくないので長時間座ることはなく、ちょっと座ってすぐ立ち作業に戻る。 背が高めのスツールなので、ステッパーの上にスツールを置いて座っても自身の足が当たらず邪魔にならないのは良い誤算だった。自動昇降デスクなので高さも合わせられる。ちなみにデスクの高さはステッパーに乗るときは 115cm, スツールに座る時は 95cm となっている。(身長 173cm の男性) ![スツール on ステッパー](/images/stepper-and-stool/1.webp) 今回ワークチェアを手放すというストイックな選択をしたのだが、2 ヶ月ほどワークチェアを脇に寄せておき移行期間を設け、なくても問題ないことを確認してから手放した。想像以上になんとかなっている。 リビングにデスクスペースがあるのだけど、ワークチェアは部屋に合わないなと感じていたのもありスッキリして良い。 ### axe-core/playwrightとmarkuplintを導入しアクセシビリティの自動テストをできるようにした Web アクセシビリティに興味があったので、まず機械的なチェックツールから学んで知識を増やそうということでこのサイトに [@axe-core/playwright](https://www.npmjs.com/package/@axe-core/playwright) と [markuplint](https://github.com/markuplint/markuplint) を導入してみました。 ### @axe-core/playwright のセットアップ 既に Playwright が導入されている状況を想定し進めます。まず[@axe-core/playwright ](https://www.npmjs.com/package/@axe-core/playwright)をインストールします。 ```sh pnpm add -D @axe-core/playwright ``` このサイトの場合 VRT として Playwright を動かしているテストがあるので([過去資料](https://speakerdeck.com/mh4gf/playwright-de-fan-xiao-sakushi-meru-vrt-to-ci-nosutetupunoxuan-ze-zhi))、そのプロセスに同居する形で axe を実行することにしました。 ```ts title="e2e.test.ts" import AxeBuilder from "@axe-core/playwright"; // [!code ++] import type { Page, TestInfo } from "@playwright/test"; const setup = async (page: Page): Promise => { // 省略 }; const screenshot = async (page: Page, testInfo: TestInfo): Promise => { // 省略 }; const testA11y = async (page: Page) => { // [!code ++] const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); // [!code ++] expect(accessibilityScanResults.violations).toEqual([]); // [!code ++] }; // [!code ++] test("VRT", async ({ page }, testInfo) => { await setup(page); await screenshot(page, testInfo); // [!code --] await Promise.all([testA11y(page), screenshot(page, testInfo)]); // [!code ++] }); ``` `accessibilityScanResults.violations` が空配列になっているかどうか、で検証します。このサイトの場合対応しきれない指摘はなかったのでこのままでよかったのですが、もし除外したい指摘がある場合は `toMatchSnapshot` にし配列の状態をそのまま保存する形になるようです。 このテストをそれぞれのページで実行し、出てきた指摘を解消していくのを進めました。 ### markuplint のセットアップ 以下のコマンドを実行し、対話式にセットアップします。 ```sh $ pnpm dlx markuplint --init Packages: +170 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Progress: resolved 170, reused 169, downloaded 1, added 170, done ┌──────────────────────────────────────┐ │ │ │ /✔\ Markuplint │ │ v4.1.0 │ │ │ │ Initialization │ │ │ └──────────────────────────────────────┘ ✔ Which do you use template engines? · React (JSX) ✔ May I install them automatically? (y/N) · true ✔ Do you customize rules? (y/N) · false ✔ Does it import the recommended config? (y/N) · true ✨Created: /Users/mh4gf/ghq/github.com/MH4GF/mysite/.markuplintrc Install automatically npm install -D --legacy-peer-deps markuplint @markuplint/jsx-parser @markuplint/react-spec npm ERR! code EUNSUPPORTEDPROTOCOL npm ERR! Unsupported URL Type "workspace:": workspace:^* npm ERR! A complete log of this run can be found in: /Users/mh4gf/.npm/_logs/2024-03-14T00_55_41_545Z-debug-0.log ``` .markuplintrc ファイルの作成はできましたが、依存のインストールでエラーになってしまいました。このプロジェクトでは pnpm を使っているのですが、npm でインストールしようとしてエラーになっているようです。 markuplint が実行したコマンドを参考に以下のパッケージをインストールします。このサイトは React で実装されているため、必要になるパッケージが含まれています。 ```sh pnpm add -D markuplint @markuplint/jsx-parser @markuplint/react-spec ``` 設定ファイルの入力中の補完が欲しいので、.markuplintrc ファイルを `.markuplintrc.ts` に変換して TypeScript で書けるようにします。 ```ts title=".markuplintrc.ts" import type { Config } from "@markuplint/ml-config"; const config: Config = { specs: { "\\.tsx?$": "@markuplint/react-spec", }, parser: { "\\.tsx?$": "@markuplint/jsx-parser", }, extends: ["markuplint:recommended"], }; export default config; ``` `Config` 型は `@markuplint/ml-config` パッケージにあるようです。このパッケージはインストールされていないため、依存に追加します。 ```sh pnpm add -D @markuplint/ml-config ``` package.json の scripts に以下を追加し、実行できるようにします。 ```json title="package.json" "lint:markuplint": "markuplint \"**/*.tsx\"", ``` これで lint が実行できるようになったので、それぞれ出てきた指摘を対応していきました。 ### 指摘を受けて対応したこと axe-playwright と markuplint それぞれで指摘が出たため、その指摘がなくなるよう解消していきました。 以下で対応したことを列挙してみます。理解が間違っている箇所があるかもしれませんが、ぜひ指摘いただけると嬉しいです。 - アクセシブルネームがなかった要素の対応 - アイコンリンクそれぞれに aria-label を設定した - 記事の見出しリンクに aria-label を設定した( 「このセクションへのリンク」にした) - ヘッダーの nav 要素の aria-label を「グローバルナビゲーション」、記事の目次の nav 要素の aria-label に「目次」を設定した - リンクカードの og 画像に alt がなかったので「`${hostname}のサムネイル画像`」を追加した - リンクカードの img タグの width と height が `100%` という指定だったが、それを整数値にした - 見出しが h2 から始まっているページがあったので h1 からに変えた - コントラスト比の改善 ... コードブロックの diff がある場合にコントラストが弱い指摘がでていたので、背景色の明度を調整した - インライン SVG に含まれていた xmlns は効果がないため削除した - 横スクロール可能なコードブロックに tabindex="0" を設定した - これは悩んだので後述します。 また指摘を受けて背景知識や対応方法を調べたり、関連記事を読む中で改善した方が良いと判断した対応や、VoiseOver で読み上げてもらった上で気になった箇所の対応も以下に掲載します。 - aria-label や代替テキストは英語にしていたが、このサイトは日本語のコンテンツなので全て日本語に変えた - 日付表記は `2024/03/16` にしていたが、VoiceOver で年月日の読み上げができないため `2024-03-16` に変えた - Web サイトの装飾目的で hr 要素を利用していたが、hr 要素はコンテンツの意味的な区切りを表し、スクリーンリーダーでも区切り線として読み上げられてしまうため div に切り替えた ### 横スクロール可能なコードブロックに tabindex="0"を設定した 指摘を受けて悩んだ事例を 1 つ紹介します。axe からの以下のような指摘を受けました。 `Ensure elements that have scrollable content are accessible by keyboard.`(id: [scrollable-region-focusable](https://dequeuniversity.com/rules/axe/4.8/scrollable-region-focusable?application=playwright)) スクロール可能なコンテンツはキーボードでのアクセスができるようにするべきというもので、これは以下のような記事内のコードブロックで指摘されていました。 ```ts const text = "コードブロックです"; ``` このコードブロックを表示している pre 要素には `overflow-x: auto` を指定しているため、コンテンツがはみ出した際に横スクロールが発生します。しかしこの要素はフォーカスを当てることができないため、キーボードでの横スクロールができないのは改善すべき、とのことでした。 [指摘内容の説明 URL](https://dequeuniversity.com/rules/axe/4.8/scrollable-region-focusable?application=playwright)にもある通り、この問題の解決には `tabindex="0"` を指定することで解決できます。pre 要素にフォーカスを当てることができるようになり、横スクロールをキーボードで行えるようになります。 しかし、pre 要素に tabindex を設定すると今度 Linter として設定していた Biome でエラーになってしまいました。 `The HTML element pre is non-interactive. Do not use tabIndex. `(rule: [lint/a11y/noNoninteractiveTabindex](https://biomejs.dev/linter/rules/no-noninteractive-tabindex)) 対話的ではない要素に tabindex を設定すべきでない、という指摘です。pre 要素は一般的に対話的ではないのでこの指摘は理にかなっているものの、今回の場合コードブロック内のコンテンツがはみ出した場合はスクロール操作が必要になるため、どうすべきか迷ってしまいました。 結論として、今回は Biome のルールを `biome-ignore` でこのコードブロックの行だけ無効にすることにしました。対話的な操作が必要なのは事実であり、ユーザーの操作を損ねないための例外パターンと判断しました。 ### 感想 思っていたより手軽に導入できたのと、ある程度は対応しきれないルールがあると想像していたのですが全て導入できてよかったです。 自動テストや静的解析として導入することでより実践的な形で知識を得られますし、どう解消するか調べる過程で関連する情報が手に入り知識が広がるのはやはり良いなと感じました。 もちろん自動テストだけではアクセシビリティを完全に担保することはできず、手動でのアクセシビリティテストも重要です。そのためにも体系的な知識をこれからつけていこうと思います。 ### 余談: axe のレポートを見やすくする axe-html-reporter について axe-playwright でのテストは、以下のように指摘が 0 で空配列かどうかで検証することになります。 ```ts title="e2e.test.ts" const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); expect(accessibilityScanResults.violations).toEqual([]); ``` 指摘がある場合、ある程度大きなオブジェクトが配列内に含まれているため、Playwright のテスト結果では読みづらいです。見やすいレポーターはないか調べてみると、axe-html-reporter を使うのが良さそうだったため紹介します。 まずプロジェクトに `axe-html-reporter` をインストールします。 ```sh pnpm add -D axe-html-reporter ``` axe によるテストの途中で、axe-html-reporter のレポート作成を挟みます。 ```ts title="e2e.test.ts" import { createHtmlReport } from "axe-html-reporter"; // [!code ++] const testA11y = async (page: Page) => { const accessibilityScanResults = await new AxeBuilder({ page }).analyze(); createHtmlReport({ // [!code ++] results: accessibilityScanResults, // [!code ++] }); // [!code ++] expect(accessibilityScanResults.violations).toEqual([]); }; ``` このように設定することで、以下のように HTML で見やすいレポートが生成されます。 ![axe-html-reporterの出力HTML](/images/axe-core-playwright-and-markuplint/1.png) ### 参考資料 - [コンポーネントをアクセシブルに保つ技術](https://zenn.dev/ubie_dev/articles/38b2b93272ee60) - [Accessibility testing | Playwright](https://playwright.dev/docs/accessibility-testing) - [aria-label を使いすぎない](https://azukiazusa.dev/blog/do-not-use-aria-label-too-much/) - [スクリーンリーダーに配慮したテキスト表記](https://azukiazusa.dev/blog/screen-reader-friendly-text-notation/) ### TPS40 (GREY/SILVER DAISY) Build Log TPS40 というカスタムキーボードのキットを組み立てたので、そのビルドログを書く。 https://deadline.space/products/tps40-keyboard TPS40 は、DEADLINE STUDIO が手がける 40%レイアウトのキーボード。日本では[遊舎工房さんがベンダーとなって Group Buy を開催しており](https://shop.yushakobo.jp/products/7556)、自分は 2023 年 5 月の GB で注文し、2023 年 10 月に届いた。 接続端子は USB Type-C で、 DAISY(横ずれ配列) と PLANCK(格子配列) の二つのレイアウトが選択できる。無線接続も可能だが、遊舎工房さんの GB 版の場合は非対応とのこと。 最近でも遊舎工房さんのツイートを見ていると何度か入荷アナウンスをしているため、手に入る機会は今後もあるかもしれない。 40%キーボードというサイズが自分は好きで、Corne Cherry V3 から自作キーボードを始め、Keyball 39 → cocot46plus → Keyball 44 と乗り換えてきた。そんな中 40%かつカスタムキーボードかつデザインが可愛いキーボードの Group Buy が開催されると聞いて、44,000 円という値段に渋っていたものの清水の舞台を飛び降りる気持ちで買ってみた。結果としてはかなりの大満足となっている。 ### Build Log さてこの TPS40 だが、製品自体はとても良いもののサポートがあまり良くないという評価を見かけるが、自分もそうだね...となってしまった。 まずビルドガイドが用意されていない。同梱物にガイドにあたる図の紙が入っているが、IKEA の説明のような図があるだけでそれだけではさっぱりわからなかった。[DEADLINE STUDIO の Discord サーバー](https://discord.com/channels/958628217730777088/997468609062383716)に TPS40 専用のチャンネルがあるのだが、そこでのビルド方法についての質問には「有志の組み立て動画があるのでそちらを見てくれ」とのこと。以下の動画が参照されていた。 https://www.bilibili.com/video/BV1oP411Y7Nr/ 他に情報は見つけられなかったため、自分もこの動画を食い入るように見ることでなんとかビルドできた。幸い不具合もなかったため自分はチャットを深追いしたりはしていないのだが、困ったことがあればチャンネルで相談するのも良いかも。 ### 組み立て中の写真 ![](/images/tps40/6.jpg) ボトムケース。これがかなり重い。 ![](/images/tps40/7.jpg) 六角形の溝にクッションシールを貼っていく ![](/images/tps40/8.jpg) おそらく「Titanium stonewash shrapnel」と呼ばれているもの。同梱されている IKEA の説明のような図ではこのパーツについての説明がされていて、PLANCK と DAISY でねじ止めする場所が違うので注意。 ![](/images/tps40/11.jpg) この図だけではなんのこっちゃとなるので、動画と見比べながら止めていく。 ![](/images/tps40/9.jpg) 溝に乗せたところ。TPS40 はカスタムキーボードでよくあるガスケットマウント方式とのことで、これは PCB を固定せず、クッションで沈み込ませることでタイピング時の底打ち感を抑えることができるとのこと。 ![](/images/tps40/10.jpg) PCB やトッププレートを乗せたところ。 ![](/images/tps40/5.jpg) 完成! ### VIA TPS40 のキーマップを変更するには [VIA](https://usevia.app) という Web アプリを使うとのこと。 自分は今まで [Remap](https://remap-keys.app/) か、 GitHub の [qmk_firmware リポジトリ](https://github.com/qmk/qmk_firmware)を手元に落としてきた上での直接ビルドしか経験したことがなかったので、今回初めて VIA を使った。 VIA とは何か?についてはこちらのサリチル酸さんの記事がとても参考になった。 https://salicylic-acid3.hatenablog.com/entry/via-manual TPS40 のキーマップは 2024/02/12 現在 VIA の Web アプリ上ですぐ使えるようにはなっておらず、自身でキーマップの JSON ファイルをアップロードして利用する必要がある。VIA の GitHub リポジトリに[プルリクエスト](https://github.com/the-via/keyboards/pull/1953)が上がっているが、まだマージされていないようだ。 そのプルリクエストに含まれている JSON ファイルをダウンロードし VIA の Web アプリでアップロードすることでキーマップの編集ができるようになる。詳しくは上記サリチル酸さんの記事に記載されているのでそちらを参照のこと。 注意点として、DAISY レイアウトの場合は `tps40a.json`、PLANCK レイアウトの場合は `tps40b.json` となる。 ### 完成 ![](/images/tps40/2.jpg) ![](/images/tps40/3.jpg) ![](/images/tps40/1.jpg) キーキャップは[PBTfans Doubleshot WOB](https://shop.yushakobo.jp/products/6349)を遊舎工房さんで見つけて購入した。完全に同色というわけではないけど、ある程度揃えられていて良い。 ### 所感 ダークグレーとシルバーのケースがとても好みで可愛い。デスク上での佇まいも良く、かなり所有欲を満たしてくれる。 打鍵音もアクリル積層ケースと比べて吸収されずっしりと鳴る感じがするのもわかる。ケースがかなりの重さのため打鍵時の安定感はありつつ、ガスケットマウントにより比較的柔らかめな打鍵感なので指も疲れにくいように感じる。 配列としては、元々使っていた Keyball 44 と大体が同じなものの一番右の修飾キー列がないため、そのあたりのキーマップを見直す必要があった程度ですぐ慣れることができた。 ということで TPS40 のビルドログだった。組み立て自体はちょっと難易度高めだったが、完成したものを見るとそれを忘れさせるくらいに満足している。かなりキーボード欲も満たされてしまったので、しばらくはこれで遊びたい。 ### Next.js App Router製ブログサイトでのツイートの埋め込み このブログサイトでツイートの埋め込みをサポートしました。 ツイートの埋め込みについてはウェブ上での情報量も多く枯れている内容かと思いきや、意外と考えることがあったため Next.js App Router での埋め込みについてまとめます。 この記事では X のポストのことをツイートという呼称に統一しています。(伝わりやすいので....) ### 埋め込みの前提知識 ツイートの埋め込みを実装する際の前提知識としては、以下の Catnose さんの記事が参考になります。 https://zenn.dev/catnose99/articles/329d7d61968efb ツイートのウェブページから[ツイートの埋め込み]を選択し、埋め込み用の HTML をコピーします。以下のような HTML が手に入ります。 ```html ``` この widgets.js が読み込まれると、twitter-tweet クラスを持つ blockquote タグが iframe に置き換わります。 以下のような表示です。 その他にも [Catnose さんの記事](https://zenn.dev/catnose99/articles/329d7d61968efb)では JavaScript フレームワークでのパフォーマンス向上などにも触れられています。 以降はこの記事を読んでいる前提で記載していきます。 ### このサイトについて 対象となるこのサイトの概要について説明します。 - Next.js App Router で作られているブログサイト - 記事はマークダウンファイルとしてリポジトリ内に保存されている - マークダウンファイルを [contentlayer](https://contentlayer.dev/) で読み込み、React Server Component 上で [Unified.js](https://unifiedjs.com/) を利用し ReactElement に変換する ソースコードはこちらです: https://github.com/MH4GF/mysite ### 基本的な考え方 ウェブ上でよく見かける React でのツイート埋め込みの記事において、 以下のように tweetId だけを props で受け取って blockquote タグを生成するコンポーネントを実装するパターンをよく見かけます。 ```tsx ``` しかしこの場合埋め込みが完了するまでツイート本文が抜け落ちてしまうため、プログレッシブエンハンスメントの観点であまり望ましくありません。 プログレッシブエンハンスメントとは、誰もが Web ページの基本的なコンテンツと機能にアクセスできるようにした上で、追加のブラウザ機能やネットワーク回線を持つユーザーはさらに強化された機能を利用できるようにする設計思想です。 ツイート埋め込みで考えると、blockquote 要素としてもツイート本文を閲覧できつつ、 widgets.js の読み込みが完了したならば iframe のリッチな表示に切り替わる方が望ましいです。 それを実現するためには、今回はツイートのウェブページから得られる HTML をそのまま利用する方が望ましい、と判断しました。 次節以降からは、本題の Next.js での実装について紹介していきます。 ### Next.js での widgets.js の読み込み まず、Next.js で widgets.js を読み込む方法について考えていきます。これは `next/script` を利用するのが良いでしょう。 ```tsx title="TwitterWidgets.tsx" import Script from "next/script"; export const TwitterWidgets = () => { return ( ``` ここの `