2024-06-20
プロダクト開発とユーザーとの摩擦
5月から6月にかけて、Chooningのアップデートを重ね、新たな機能を続々とリリースした。アーティストのアカウント認証機能、プロフィール共有機能、Spotify視聴履歴に基づいたおすすめ投稿機能などを実装し、さらに中国語(繁体字)版インターフェースもリリースして、この夏は台湾市場への進出も見据えている。
しかし、どんな施策も万人に受け入れられるわけではない。アプリのアップデートは歓迎されるばかりではなく、ときに「改悪だ」という厳しい意見も寄せられる。開発者は、データベースに記録される値や計測ツールの結果、インタビューを通じたユーザーの声など、様々な情報のもとに開発内容を決めている。それでも、すべてのユーザーを満足させることはできない。長くサービスを続けていれば、開発者とユーザーの間には大なり小なり摩擦が生じる。
一般的によくあるのは、サービス側が収益性を高めようとする施策だろう。例えば、目立つ場所に広告を配置したり、広告の表示頻度を上げたりすればユーザーは不快に思う。動画や漫画の続きを見ようとして、下手くそなゲームのプレイ動画を数十秒見せられてイライラした経験をしたことのある人は多いだろう。Chooningにはそういった仕組みはないが、僕は会社員としてプロダクトを作っていたとき、この手の実装をする際には心苦しく思っていたし、実際にリリース後のSNSでユーザーの毒づく声を目にしてよく落ち込んでいた。
Chooningでは収益性を求める施策は行っていないものの、だからといってユーザーの不満を買わずにやれているわけではない。むしろ、より切実な衝突を感じることがある。例えば、ユーザーのプロフィールをシェアできるようにした件については「お互いにフォローせず、時々お邪魔するのが楽しみだったのに」といった声が上がった(恐らく相互フォローを促進する意図があると受け止められたのだろう)。また、おすすめフィードを実装した件については「ファーストビューがメジャーなアーティストで埋め尽くされてしまい、自分の好きなマイナーアーティストを紹介しても届きづらくなった」という声が上がった。
僕は常々、自分のプロダクトは技術や機能だけでなく、価値観の次元で勝負したいと思っている。Chooningでユーザーが体験できることは、「人と音楽との関わり方はこうあってほしい」という僕の価値観に基づいている。具体的には、Spotifyなどのサブスク(ストリーミング)サービスで音楽を手軽に聴きながらも、その音楽について向き合って考えたり、些細な思い出を語ったり、同好者と共感し合う時間を持って欲しい、というものだ。そのためにユーザーがスムーズに音楽を共有したり、見つけたり、交流したりできる機能を充実させている。僕のようなタイプの開発者には、そうやって価値観を形にしたものに触れてもらい、それを心地よいと感じてもらいたいと思う欲望がある。機能の追加や変更は、自分の価値観をより明確に伝えるためのメッセージでもある。
だからこそ、仕様を変更したときに、それまで支持してくれていたユーザーが「これは求めているものじゃない」と言って離れていくのを見ると、価値観に共感してもらえなかった…という極めて個人的な喪失感を感じてしまう。それは、収益性の都合でユーザーに不満を持たれるよりもはるかに悲しい。収益性の向上はサービスの存続に不可欠であり、僕も運営者としての責務の一部として割り切ることができる。しかし、価値観の相違による意見の衝突は自分の気持ちの解決が難しい。「ごめんな、でも僕はこう思うんだ。分かってくれることがあれば嬉しい」と目を閉じるしかない。
離れていってしまったユーザーのことは、いつも少し心に棘が刺さったような気持ちで残っている。自分のプロダクトを作るということは、自分の価値観を形にして社会に届けるということだ。そこでは多くの「Not for me」の声も受けることになる。ストアからダウンロードした僕のアプリを、翌月も使い続けてくれる人がどれくらいいるだろう? その数字を見る度に、僕は他者との価値観の違いを認識する。あえて強がってみせるなら、それこそが、本気で物を作ることの面白さだと言えるかもしれない。
プロダクトには成長していく段階があり、それに伴って作り手と使い手の関係も変わっていく。ある時期を共に過ごしたユーザーの一部が次の時期に離れていくことは避けられない。頭では理解しているのだが、それでも、一度プロダクトを気に入ってくれたユーザーには、ずっと使い続けてもらいたいと願ってしまう。
ユーザーの声は大きい。少なくとも僕は、どんなに厳しい意見や的外れな批判でも、自分が生み出したものに対して感情を込めて意見をくれる人には感謝している。高校時代にゲームを作ったり、大学時代に友達とサービスを作っていた頃は、世の中の反応は全くなかった。それに比べれば、不満の声であったとしても「反応がある」というのは嬉しいことだ。作り手のモチベーションは、案外そんなものに支えられている。
2024-06-09
Hugo + Netlify + DecapCMS を使ったサイト構築
35歳の誕生日を機に、個人サイト(ブログ)をリニューアルすることにした。
以前より、Hugo + Netlify を使って個人サイトを配信していたのだが、この仕組みには「記事の更新作業に手間がかかる」という問題があった。
静的サイトジェネレーター兼コンテンツ管理として Hugo があり、Netlify がホスティングを担っているのだが、実際に記事を更新する際は、次のような手順となる。
- Github リポジトリを pull する(僕は複数台のマシンを使っているので、ローカルを最新状態に同期する必要がある。)
- mdファイルを追加し、そこにブログ記事を書く
- Github に push する
- Netlify が更新を検知し、build → deploy を実行する(=サイトが更新される)
この手順から、Github に対する操作を省きたい。つまり、一般的なブログサービスのように(あるいは Wordpress サイトのように)「ブラウザで管理画面を開き、記事を更新する」という手続きにしたい。
そこで Decap CMS(旧・Netlify CMS)を使ってみることにした。ヘッドレスCMSの代表格で、特にGitベースであるという点が大きい(コンテンツの変更履歴を Git で管理できる)。
また、併せてサイトのデザインも一新することにした。以前は Hugo に不慣れなこともあり、無料配布されていた Hugo テーマを流用して改造しながら使っていたのだが、その運用を4〜5年ほどしたことで Hugo の仕組みにもだいぶ詳しくなった。そこで Hugo テーマをフルスクラッチで作ってみることにした。
各ツールの役割の確認
- Hugo : 静的サイトジェネレーター。Markdown ファイルを HTML に変換して静的なウェブサイトを生成してくれる。静的サイトにはロマンがある。Golangで書かれているため動作が高速で、Goテンプレートエンジンによる柔軟なテンプレートシステムが魅力的。
- Netlify : 静的サイトのホスティングサービス。Git リポジトリと連携して自動デプロイを行う。
- Decap CMS : オープンソースのヘッドレスCMS。GUIベースの管理画面を提供し、静的サイトジェネレーターと連携してコンテンツを管理する。実装後に重大な落とし穴が発覚する(後述)。
1. サイトの基本形を作成
まずはまっさらな状態から Hugo プロジェクトを作っていく。
1. Hugo プロジェクトを作成
hugo new site ezeroms.com
cd ezeroms.com
2. カスタムテーマの準備
Hugo プロジェクトで推奨されている形は、root ディレクトリの下に /site
とかで区切って、テーマ等を置くディレクトリを一段下で管理する方法だ。これは、root ディレクトリでサイト設計やデプロイに関するメタ情報を管理し、具体的なサイト自身の設定や見た目と分離することで、複数テーマも管理やスケーラビリティを確保しようとするものだ。
しかし、今回のサイトの場合はテーマを切り替えることはない(自分でアップデートし続ける)し、プロジェクトの規模も小さいので、構造のシンプルさを優先して全て root ディレクトリ直下で管理する方法にした。
ezeroms.com
├── archetypes
│ └── index.html
├── content
│ ├── about
│ │ └── _index.md
│ ├── diary
│ │ └── _index.md
│ ├── shoulders-of-giants
│ │ └── _index.md
│ └── work
│ └── _index.md
├── layouts
│ ├── _default
│ │ ├── baseof.html
│ │ └── rss.xml
│ ├── about
│ │ └── index.html
│ ├── diary
│ │ ├── index.html
│ │ └── single.html
│ ├── month
│ │ └── list.html
│ ├── partials
│ │ ├── header.html
│ │ ├── footer.html
│ │ ├── global-nav.html
│ │ └── ...
│ ├── shoulders-of-giants
│ │ └── index.html
│ ├── subject
│ │ └── list.html
│ ├── topic
│ │ └── list.html
│ │── work
│ │ ├── index.html
│ │ └── single.html
│ └── index.html
├── public
├── static
│ ├── admin
│ │ ├── config.yml
│ │ └── index.html
│ ├── css
│ └── images
├── config.toml
└── netlify.toml
- public : hugo server で build されたファイルが格納される場所。キャッシュが邪魔しているのか?と思ったときは全削除したりする。
- content/*/_index.md : 一見不要そうだが、ここに index の md ファイルがないとディレクトリを正しく認識してくれない。
- static/admin : Decap CMS の管理画面用のファイル群。
2. 本番環境と自動デプロイの設定
1. GitHub リポジトリの作成
git init
git add .
git commit -m "Initial commit"
git remote add origin <GITHUB_REPO_URL>
git push -u origin main
2. Netlify アカウントの作成
3. Githubリポジトリの連携
Netlify の管理画面で「New site from Git」を選択し、GitHubリポジトリを連携。
4. ビルド設定
Build Command: hugo
Publish Directory: public
これで、Github の main ブランチが更新されると、Netlify側で自動デプロイが走るようになる。
5. ドメイン設定
Netlify の管理画面で ezeroms.com
を割り当てる。
3. 詳細なサイト構築(テーマファイルの編集)
layouts 以下のテーマファイルと static/css をひらすら編集し、理想的なデザインを作り上げていく。自分一人のプロジェクトということもあり、あらかじめ全てのページを Figma で作ることはせず、主だったページのレイアウト構成だけ Figma 上で検証して、スタイリングについては実装しながら検討していった。褒められた方法ではないが、こういうところがソロ・プロジェクトの爽快なところだ。
コツ : テーマファイルへのマーキング
hugo server で localhost を確認しながら作っていくのだが、描画されているページに適切なテーマファイルが当たっているのかどうか分からなくなる。(「このページにこのテーマファイルが当たってほしいのに当たらん〜〜〜〜」的なことが続く)
そこで、すべてのテーマファイルに以下のようなマーキングした上で、config.toml の設定と、テーマファイルのディレクトリやファイル名を少しずつ弄りながら検証していった。
<!-- debug-info -->
<p class="debug-info">File : /layouts/subject/list.html</p>
最終的にこんなかんじで出来上がる。
課題 : URLスキーム
本当はもっとURLスキームに拘りたかったのだが、どうやってもできず(Decap CMS管理の範囲外を作ることになり)断念した。いい方法を知っている人がいたら教えてほしい。
# 理想的なURLスキーム
ezeroms.com/diary/
ezeroms.com/diary/subject/news/
ezeroms.com/diary/month/2024-06/
# 実際のURLスキーム
ezeroms.com/diary/
ezeroms.com/subject/news/
ezeroms.com/month/2024-06/
4. CMSを導入してブラウザから更新できるようにする
package.json を作成し、Decap CMSをインストール。
npm init -y
npm install netlify-cms-app
static/admin ディレクトリの config.yml 、index.html を編集してセットアップし、ezeroms.com/admin にアクセスすれば管理画面が使えるようになる。めっちゃ簡単だ。
管理画面の自由度がとても高く config.yml で設定すると一覧の表示項目も柔軟に変更することができる。
backend:
name: git-gateway
branch: main
media_folder: "/static/images/uploads"
public_folder: "/images/uploads"
collections:
- name: "about"
label: "About"
folder: "content/about"
create: true
slug: "{{fields.slug}}"
fields:
- { label: "Body", name: "body", widget: "markdown" }
summary: "{{body}}"
- name: "diary"
label: "Diary"
folder: "content/diary"
create: true
slug: "{{fields.slug}}"
media_folder: "/static/images/diary/{{fields.slug}}"
public_folder: "/images/diary/{{fields.slug}}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Date", name: "date", widget: "datetime", date_format: "YYYY-MM-DD", time_format: false }
- { label: "Slug", name: "slug", widget: "string" }
- { label: "Month", name: "month", widget: "list" }
- { label: "Subject", name: "subject", widget: "select", multiple: true, options: ["news", "music", "manga-and-anime", "movies-and-dramas", "comedy", "gaming", "sports", "books-and-magazines", "languages-and-foreign-cultures", "design-and-creative", "internet-and-technology", "natural-science", "humanities-and-social-sciences"] }
- { label: "Body", name: "body", widget: "markdown" }
summary: "{{date | date('YYYY-MM-DD')}} | {{body | truncate(280, '...')}}"
sort:
field: "date"
direction: "desc"
- name: "work"
label: "Work"
folder: "content/work"
create: true
slug: "{{fields.slug}}"
media_folder: "/static/images/work/{{fields.slug}}"
public_folder: "/images/work/{{fields.slug}}"
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Date", name: "date", widget: "datetime", date_format: "YYYY-MM-DD", time_format: false }
- { label: "Slug", name: "slug", widget: "string" }
- { label: "Image", name: "image", widget: "image" }
- { label: "Body", name: "body", widget: "markdown" }
summary: "{{date | date('YYYY-MM-DD')}} | {{title}}"
sort:
field: "date"
direction: "desc"
- name: "shoulders-of-giants"
label: "The shoulders of Giants"
folder: "content/shoulders-of-giants"
create: true
slug: "{{fields.slug}}"
fields:
- { label: "Topic", name: "topic", widget: "list"}
- { label: "Body", name: "body", widget: "markdown" }
summary: "{{body | truncate(280, '...')}} Topic {{topic}}"
sort:
field: "body"
direction: "asc"
Decap CMS の落とし穴
さて、これで理想的な個人ブログの運用体制が構築できた…と思っていたのだが、ここで Decap CMS の大きな落とし穴が発覚する。
記事のテキストフィールドが、日本語入力に対応できていないのだ。漢字変換をしようとしたときにキャレットの位置がズレてしまい、正常に入力作業をすることができない。
Updating Slate editor to support Korean
どうやら Slate のバージョンが古いのが原因らしく、改善のためのプルリクは出ているのだがスルーされているっぽい。
(写真)
Issueが立っていたのでむなしくコメントを残しておいた。
なので、このサイトの実際の運用は次のようになっている。
- UpNote でコンテンツ管理(UpNote はモバイル用のネイティブアプリが用意されている)
- UpNote から DecapCMS にコピペして記事を更新
記事の編集作業部分をテキストエディタに外出ししている状態だ。以前に比べれば格段に更新しやすくはなったものの、できればブラウザで完結する運用を目指したい。
もちろん、ヘッドレスCMSには Decap CMS 以外にも選択肢はあるのだが、Netlify との相性と「無料で使える」という条件で探すと少し絞られてしまう(APIでやりとりするものは使いたくない)。また、できればスマホから更新ができるように、モバイル用インターフェースが提供されているとなお嬉しい。もう少し調べてみようと思う。