🚀

OSSのアプリストア管理ツール、AppAgentを作りました

2025/01/20に公開

先日アプリストア最適化(ASO)からリリースまで一気通貫でいけるウェブサイトAppAgentを公開しました(Google Playは未対応)。

GitHubはこちらです。ぜひスターをつけていただけると嬉しいです!

https://github.com/ngo275/app-agent

開発に至った経緯

僕は趣味でいくつかアプリを作っており、以前、ASOを改善するために、いくつかのASOツールを試してみました。その中でもApp Radarには月80ドルのプランで有料機能も試してみました。たしかにキーワードのスコアや他のアプリのキーワードなど、情報としては価値が高いと思ったものの、個人開発者が使いこなすには、高価でオーバースペックだという印象を受けました。また、複数の言語に対応させようとすると、UXがひどく、相当な時間がかかることを痛感しました。App Store Connectも同様に、複数の言語に対応させようとすると、とにかくページ移動が多く手間が増えるのが辛いです。

試してみた結果、自分が欲しいのは、App RadarやAppFollowのようなASOを徹底的に深ぼるためのASOツールというよりは、個人でアプリをリリースするにあたって面倒な作業(ASO、App Store Connectの更新)の簡易化もしくは自動化だと実感しました。

AppAgentで何ができるのか

AppAgentは大きく2つの機能があります。ASOとリリースです。

ASO

  1. キーワード自動選定

自動で任意の言語においてキーワードを調査、選定をします。App StoreのパブリックAPIを活用して、キーワードのトラフィックや難易度を推定します。数十のキーワード候補においてスコアリングをして最終的にフォーカスすべきキーワードを絞り込みます。

  1. タイトル、サブタイトル、概要の生成

選定したキーワードをうまく散りばめたタイトル、サブタイトル、概要を作成します。どの言語でも作成可能です。

リリース

適当にリリースノートを入力すると(どの言語でも良い)、アプリがサポートしている全言語に自動で、文章のボリュームを増やしつつローカライズします。

さらに、AppAgentで入力した全情報(タイトル、サブタイトル、キーワード、概要、リリースノート)をApp Store Connect(Google Play Consoleは未対応)に同期します。僕はわざわざChatGPTから毎回コピペしていたのでこのプロセスが大嫌いで自動化しました。

最後に、App Store Connectのレビューの申請もAppAgentからできるようにしました。このボタンをクリックすると、アップロードしたビルドを選択し、その後審査の申請も行います。

ASOのツールと言うよりは、アプリを開発してリリースするまでのプロセスにおいて、ASOを含めリリース周りも一気通貫で楽にできる、というツールです。

オープンソース

AppAgentはオープンソースで開発しています。

https://github.com/ngo275/app-agent

オープンソースにした理由は以下の通りです。

  • ASOまわり、Google Play Consoleまわりは自分ひとりでよい質のものを作るのが難しい
  • AppAgentのターゲットはマーケターではなくエンジニアなので、アプリのリリースやASOにまつわるツラミを共感してもらって一緒に作っていきたい
  • 人によっては、App Store ConnectのAPI Keyを外部に渡すのが嫌な人や、データを外部に見られたくない人がいたらセルフホスティングしたいだろうと思った
  • オープンソースの方がコードレベルで見られるので頑張れる
  • OSS Alternativeみたいなのが増えている(Google AnalyticsのOSS版、HubSpotのOSS版、みたいなのがだいたい出揃っている)中、App RadarのOSS版はまだなかった

共感した人がいればぜ、励みになるのでGitHubでスターだけでもつけていただけると大変嬉しいです!

技術的な話

安さと利便性を追求したスタック

今回はPapermarkというOSSのリポジトリを参考にしました。US系のスタートアップが利用しているテックスタックを学ぶことができ、自分の勉強にもなりました。今回気をつけたことはいかにしてランニングコストを安くするか、ということでした。

色々調べた結果、以下のツールでAppAgentを構築しています。

  • Vercel (Nextjs)
    • サーバーのホスティングやデプロイの自動化周りに利用。リソースを食う関数があるので20ドル払っているが、大体のサービスのローンチ初期は無料プランで十分そう。
  • OpenAI (LLM)
    • 従量課金(クレジット制の前払い)なので、初期はそんなにコストもかからない。DeepSeekのように安いLLMサービスが出てきてはいるが、サービスの安定性の観点からOpenAIに軍配が上がる印象。
  • Upstash (Redis)
    • Redisやベクトル検索用のサーバー、メッセージングのサーバー。初期のフェーズだとこちらも無料プランでいける。
  • Neon (PostgreSQL)
    • PostreSQLに関してはPlanetScaleが以前までは無料プランがあったが、今はNeonが一番無料枠が寛容だったので採用。
  • Resend (Email)
    • EmailをRestAPIで送る便利なツール。こちらも初期フェーズは無料。何より、サービスが使いやすい。Reactでメールテンプレートの管理ができ、i18nもフロントエンドのコード同様に利用できる。詳細は後述。
  • PostHog (Analytics)
    • Google AnalyticsのOSS版。アカウントあたり1プロジェクトは無料。とても開発者フレンドリーで使いやすい。詳細は後述。
  • Stripe (Payment)
    • 言わずもがな課金ツール。よくできているサービス。
  • Prisma (ORM)
    • TypeScriptでPostgreSQLを楽に扱えるORM。

NextAuthで爆速開発した認証

NextAuthを利用し、認証部分の開発工数をミニマムに抑えられました。NextAuthでは、以下のステップだけで認証機能が実現できるので便利です。

  • ドキュメントに指定されているように、データベースにテーブルを作る
  • 認証に利用するプロバイダ(GoogleやGitHubなど)の設定をする
  • authOptionsにサービス独自の実装をする
  • loginページの実装をする

Stripeを利用した月額課金の実装

関連するコードは以下のページです。ほとんどコピペでStripeでの月額課金の実装が完了すると思います。フロントエンドはStripeのセッションを作成し、Stripeの支払画面に遷移させます。その後は、Stripeから飛んでくるWebhookをうまく制御する感じです。

LLMを使ったコンテンツ生成の実装

以下にて、このプロジェクトで利用しているプロンプトや利用しているモデルの情報が確認できます。

https://github.com/ngo275/app-agent/tree/main/src/lib/llm

Structured Outputsを利用することで、JSON形式で欲しい情報を取得しています。かなり便利です。

基本的にプロンプトは英語で記述しているのですが、英語プロンプトによって生成される日本語の文章の質が低かったのでo1-previewを利用しています。o1-minigpt-4oではかなり違和感があり実用にはキツかったです。日本語だけではなく、どの言語にも対応させたかったので賢いモデルを使うことで解決しています。

EmailのUIと多言語管理

今回始めてResendを使ったのですが、とても使い勝手が良かったです。以下のコードは実際にAppAgentで使っているWelcomeメールです。AppAgentのUIで利用している多言語対応のライブラリをメールでもそのまま利用できています。メールのUIもReactで表現できるので大変便利です。

import React from 'react';

import {
  Body,
  Button,
  Container,
  Head,
  Hr,
  Html,
  Link,
  Preview,
  Section,
  Tailwind,
  Text,
} from '@react-email/components';
import { NEXT_PUBLIC_BASE_URL } from '@/lib/config';
import { CALL_LINK, GITHUB_LINK, X_LINK } from '@/lib/constants';
import { createTranslator } from 'next-intl';

interface WelcomeEmailProps {
  locale: string;
  name: string | null | undefined;
}

const WelcomeEmail = async ({ locale, name }: WelcomeEmailProps) => {
  const t = createTranslator({
    locale,
    messages: (await import(`../../../locales/${locale}.json`)).default,
    namespace: 'emails.welcome',
  });
  const tCommon = createTranslator({
    locale,
    messages: (await import(`../../../locales/${locale}.json`)).default,
    namespace: 'emails.common',
  });
  const previewText = t('title');

  return (
    <Html>
      <Head />
      <Preview>{previewText}</Preview>
      <Tailwind>
        <Body className="mx-auto my-auto bg-white font-sans">
          <Container className="mx-auto my-10 max-w-[500px] rounded border border-solid border-gray-200 px-10 py-5">
            <Text className="mx-0 mb-8 mt-4 p-0 text-center text-2xl font-normal">
              {t('title')}
            </Text>
            <Text className="text-sm">
              {t('description', { name: name ? `, ${name}` : '' })}
            </Text>
            <Text className="text-sm">{t('message')}</Text>
            <Text className="text-sm">{t('get-started')}</Text>
            <Text className="text-sm">
              <ul className="list-inside list-disc text-sm">
                <li>{t('upload-your-app-store-connect-api-key')}</li>
                <li>{t('run-an-autonomous-keyword-research')}</li>
                <li>{t('generate-aso-contents')}</li>
              </ul>
            </Text>
            <Section className="mb-[32px] mt-[32px] text-center">
              <Button
                className="rounded bg-black text-center text-xs font-semibold text-white no-underline"
                href={`${NEXT_PUBLIC_BASE_URL}/dashboard`}
                style={{ padding: '12px 20px' }}
              >
                {t('get-started-button')}
              </Button>
            </Section>
            <Section>
              <Text className="text-sm"></Text>
              <Text className="text-sm">
                <ul className="list-inside list-disc text-sm">
                  <li>
                    {t('star-the-repo')}
                    <Link href={GITHUB_LINK} target="_blank">
                      GitHub
                    </Link>
                  </li>
                  <li>
                    Follow the journey on{' '}
                    <Link href={X_LINK} target="_blank">
                      {t('x-link')}
                    </Link>
                  </li>
                </ul>
              </Text>
            </Section>
            <Section className="mt-4">
              <Text className="text-sm">{tCommon('questions')}</Text>
              <Text className="text-sm text-gray-400">
                {tCommon('shu-from-appagent')}
              </Text>
            </Section>
            <Hr />
            <Section className="mt-8 text-gray-400">
              <Text className="text-xs">
                {tCommon('copyright', { year: new Date().getFullYear() })}
              </Text>
            </Section>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  );
};

export default WelcomeEmail;

PostHogを利用したアナリティクス機能の実装

Google Analyticsは機能も多く魔境みたいになっていて好きじゃありませんでした(エンジニアで好きな人はいるのだろうか...)。PostHogはまさにGoogle AnalyticsのアンチテーゼみたいなOSSで共感できたので試してみました。イベントのトラッキングもでき、PostHogのダッシュボードも簡単にカスタマイズできるので、使い勝手は今のところ良いです。

Analyticsのセットアップの実装のあと、あとはフロントエンド側で、trackAnalyticsを利用するだけです。

Framer motionとshadcn/uiでモダンなUI実装

https://motion.dev/

Motion(Framer motion)を利用してUIのアニメーションを実装しました。Cursorで実装しながら、アニメーションをつけて、と指示するだけでかなりそれぽいのができます。

https://ui.shadcn.com/

AppAgentはUIライブラリとしてshadcn/uiを利用しました。Vercelのようなモノクロ基調なモダンなUIが簡単に実装できます。

MDXでマークダウンの実装

AppAgentの規約に関する情報はすべてマークダウンで管理しています。上記の実装のようにするだけでマークダウンで記述されたページが利用できます。

next-intlで多言語対応

https://next-intl.dev/

AppAgentはnext-intlを利用して多言語対応しています。ドキュメントに書いてある通りに進めていくと詰まることなく設定できました。en.jsonja.jsonにその言語の翻訳内容がすべて格納されています。

まとめ

個人でサービスをサクッと作ってリリースするにはコスト周りやセットアップ・管理の簡便性が重要になりますが、AppAgentはそこらへんを調べてセットアップしたので参考になれば幸いです。

また、SaaSに必要な機能(課金、認証、多言語対応など)も盛り込んであるので、これから何か作っていきたい方の参考になれば、と思います。

Discussion