はじめに
iOS / macOS 向けに個人開発している Zettelkasten 式ノートアプリ「ZettelkastenNote」の公式サイトを作りました。これまでストアページと、アプリ内のお知らせから開く LP しかなかったので、検索から来た人の受け皿と、Mac 版への導線を兼ねたページが欲しかったのが動機です。
公式サイト: https://zettelkastennote.app.shoet.team/
この記事では、構成と、作ってみて分かったことをまとめます。
目次
- なぜ作ったか
- サイトの紹介
- 使った技術
- こだわった点
- 反省点・所感
なぜ作ったか
ZettelkastenNote は App Store と Mac App Store で配信していて、ユーザーとの接点はほぼストアページだけでした。ただストアページには制約が多く、
- 動画や画像の見せ方を自由に組めない
- 言語ごとの出し分けが App Store のロケールに縛られる
- 「どこから来た人がダウンロードしたか」を細かく追えない
といった不満がありました。また Mac 版は買い切りの Pro があり、iPhone ユーザーに Mac 版の存在を知ってもらうことが収益に直結するので、ストアの外に「iPhone でも Mac でも」を伝える場所が必要でした。
サイトの紹介
1 ページ構成の LP で、日本語・英語・簡体字中国語の 3 言語を /、/en/、/zh-Hans/ で出し分けています。ヒーローには機能紹介の動画、その下に主要機能をストアのスクリーンショットで並べ、Mac 版の紹介、プライバシー方針、と続きます。
最近、「Zettelkasten とは」「よくある質問」の節と、ガイド記事を足しました。
使った技術
- フロント: Astro 5(静的生成)
- ホスティング: S3 + CloudFront(OAC)、Route 53
- IaC: AWS CDK(Go)
- CI/CD: GitHub Actions(OIDC で AWS ロールを引き受け、feature ブランチは dev、main は prod へ)
- 計測: GA4、App Store Connect のキャンペーンリンク、Google Search Console
- 動画: Remotion(アプリの画面収録を編集して書き出し)
アプリのバックエンド(Go / Lambda)と同じリポジトリの web/ に置き、CDK のスタックも同じ場所で管理しています。
Astro にした理由
中身はほぼ静的な LP なので、フレームワークのランタイムを配りたくありませんでした。Astro はビルド時に HTML を吐き、JS はほとんど出ません。画像は astro:assets で WebP に変換し、@astrojs/sitemap に i18n の設定を渡すと hreflang 付きの sitemap も出てくれます。1 ページの LP に対してはちょうどよい重さでした。
S3 + CloudFront + CDK
配信は S3 のバケットを CloudFront の OAC 経由で公開する定番の構成です。CDK の BucketDeployment でアップロードするときに、_astro/ 配下のハッシュ付きアセットは max-age=31536000, immutable、HTML と robots / sitemap は max-age=300 と、キャッシュ制御を分けています。
dev 環境は検索エンジンに載せたくないので、CloudFront Function で Basic 認証をかけ、Astro 側でも noindex を出すようにしました。認証情報は SSM Parameter Store から読んで、CDK が CloudFront Function のコードに埋め込みます。
GitHub Actions からのデプロイ
長期の認証情報を置きたくなかったので、OIDC で AWS のロールを引き受ける形にしました。ロールには CDK のブートストラップが作ったロールを AssumeRole する権限だけを持たせています。途中、Lambda のコンテナイメージが使う ECR Public のベースイメージを匿名で pull していて 429 Too Many Requests で落ちることが続いたので、ワークフローで aws ecr-public get-login-password でログインしてから pull するようにして解消しました。
こだわった点
ストアへの導線を計測する
ストアバッジのリンクは App Store Connect のキャンペーンリンク(?pt=...&ct=...)にして、ct にサイト内の位置(ヒーロー、Mac の節、ページ下部)と言語を入れています。これで App Analytics 側で「サイトのどこから来てダウンロードしたか」が分かります。GA4 側ではバッジのクリックを store_click イベントとして送り、URL の UTM を付けて SNS からの流入と組み合わせて見られるようにしました。
言語別の URL と hreflang
ユーザーは日本・中国・米国が多く、アプリも 3 言語に対応しているので、サイトも最初から 3 言語にしました。パスで言語を分け、<link rel="alternate" hreflang> と x-default を全ページに出しています。ストアのバッジも言語ごとに Apple 公式の画像を使い分けています。
動画は Remotion で
ヒーローの動画は、シミュレーターで撮った画面収録を Remotion で編集しています。カット割りと字幕をコードで持てるので、機能が増えたときに差し替えやすく、言語ごとの書き出しも同じ定義から行えます。
反省点・所感
公開しただけでは検索に載らない
公開して 3 週間、Search Console は表示回数 0 のままでした。URL 検査を見ると「クロール済み - インデックス未登録」で、技術的な問題ではなく「登録する価値がない」と判断されていました。1 ページの LP は本文が 1,000 字ほどしかなく、どこからもリンクされていない。Google から見ればストアページの薄い複製です。
これを受けて本文を増やし、ガイド記事を足し、このブログからもリンクを張る、というのが今やっていることです。LP を作る前に「検索から来てもらうなら、読む価値のあるページが要る」と分かっていれば、最初から記事ページ込みで設計していたと思います。
favicon.ico を置き換え忘れた
Astro のテンプレートに入っている favicon.ico をそのままにしていて、画像や XML を直接開いたときに Astro のロゴが出ていました。HTML からは favicon.svg を参照していたので気づくのが遅れました。しかも 1 年キャッシュの対象にしていたので、直したあともブラウザに古いものが残ります。中身が変わりうるファイルは長いキャッシュから外すべきでした。
良かったこと
CDK とワークフローをアプリのバックエンドと同じリポジトリに置いたので、「LP を直して push したら dev で見られる」が最初から動き、文言の調整が楽でした。ストアの外に自分の場所があると、Pinterest のような外部からの流入先を作るときにも、リンク先として使えます。
まとめ
Astro + S3/CloudFront + CDK で、個人開発アプリの公式サイトを 3 言語で作りました。構成は軽く、運用も楽です。一方で「作れば検索に載る」わけではなく、そこからが本番だということを実感しています。Zettelkasten に興味がある方は、ぜひ公式サイトとガイド記事を見てみてください。