🌱

【個人開発】回答すると植物が育つアンケートサービスを作りました

2025/03/10に公開

はじめに

はじめまして、たつやと申します。

突然ですが皆さん

カレーは辛さは何派ですか?私は中辛です。
朝は何を食べますか?私はパンです。

このように何派か知りたいことであったり、もっとニッチなことについて他の人がどのようにしているのか・感じているのかを知ることができるBloom Surveyというシンプルなアンケートサービスを開発・リリースしました。

実際のアンケートが以下です。

🍛カレーは辛さは何派?
https://bloomsurvey.com/pot/cm7m1yu6z0001s60dtwz0rrlk

🥣朝は何食べる?
https://bloomsurvey.com/pot/cm7nddmuy0001s60d3yrg8axf

機能は制限されますが、無料で登録無しでも使えるのでぜひ触ってみてください!

目的

Tailwind CSSを勉強したので、そのアウトプットです。
最近(でもないですが)Tailwind CSSの公式に載っているクラスを一通り動かして本にまとめました。この知識を活かしてどこまで実装できるのか挑戦してみました。

本にまとめたものはこちらです。
https://zenn.dev/tacchan5424/books/22d87ed6bc8550

題材がないとアウトプットできないので、Xのアンケート機能に注目しました(正直なんでもよかったので当時目についたものにしました)。Xのアンケート機能で過去に作成したアンケートや投票したアンケートの結果は、過去のポストを遡る必要があります。
そのため作ったアンケートと投票したアンケートを一覧化して確認できるようなサービスを作ろうと考えました。

技術選定

あまりお金をかけずに勉強できる構成の一例として参考程度に見てください。因みにですが2月は開発のみで21円かかりました。

フロントエンド

React
Tailwind CSSと言えばみたいなところがあったので採用しました。いくつか開発してきた中で多く採用していて、少し慣れていることも理由です。しかし小規模な開発にとどまっていることもあり、知らないことが多くある印象です。勉強のためTailwind CSSと同様に本にまとめてみようかなと考え中です。

余談

create-react-appがとうとう非推奨になりましたね。
https://github.com/facebook/create-react-app/pull/17003

Tailwind CSS
今回の開発のメインです。時間的な制約があり、少々作りこみが甘い感が否めません。ただ、概ね思ったように実装できたので勉強した成果をかなり実感しました。

TypeScript
2年ぶりくらいに触りました。anyを使わない縛りをしましたが私にはまだ早かったです。ちょこちょこと使ってしまっています。

バックエンド

Next.js
夏頃使っていて記憶に新しいので採用しました。次回以降、他フレームワークも使ってみたいです。

Prisma
初めて使いました。
Supabaseを操作するために採用しました。かなり簡単でよかったです。次も使いたいです。

DB

Supabase
初めて使いました。
これまで個人開発はMongDBDynamoDBを触っていました。No SQLに慣れず、両者ともイマイチだったので今回はSupabaseを採用しました。今のところはとても良いです。次も使いたいです。

認証

NextAuth
Next.jsを使っていること、過去に使ったことがあるため採用しました。実装は問題なかったのですがデプロイ時に問題が発生したためサーバーの変更を余儀なくされました。

古いですがこちらも本にまとめています。
https://zenn.dev/tacchan5424/books/d1a857f228afdf

サーバー

Cloud Run
初めて使いました。
サービスを載せるサーバだけではなくログ周りもまとめてしまいたいと考えた結果GCPにたどり着きました。(AWSは以前使ったことがあったので今回は選定の対象外にしました。)

候補としてGoogle App EngineCloud Runで悩みましたがCloud Runを採用しました。簡単に調べた限りだとCloud Runの方が色々柔軟だったためです。

余談

初詣で引いたおみくじが大吉で「思うがままに成し遂げる。」みたいなことが書いていました。これが本当ならこのサービスはバズるだろう思い、下心もりもりで柔軟な方を採用しました。

CI/CD

Cloud Build
初めて使いました。
Dockerfileに従ってソースコードをビルドして、コンテナイメージを作成します。

コンテナ管理

Artifact Registry
初めて使いました。
Cloud Buildで作成したコンテナイメージを管理しています。このコンテナイメージをCloud Runで設定してデプロイしています。

ログ

Cloud Logging
初めて使いました。
ログ取得のため採用しています。

ストレージ

Cloud Storage
初めて使いました(まだ使っていません)。
稼働してから日数が経っていたいのでまだ稼働していませんがログ保管用です。一定期間が経ったらログを移行させる想定です。

サービス概要

Bloom Surveyという名前のアンケートサービスです。知りたい物事や質問を鉢、各選択肢を苗、投票することを水をあげることに見立てました。

既に遷移して見ていただいたかもしれませんが、鉢が「カレーの辛さは何派?」、苗が「甘口/中辛/辛口」にあたります。水をあげた苗は花が咲きます。水をあげた数は水滴の中の数字でも確認できますが、多いほど苗が大きくなっています。

鉢にはサービス上で全ユーザが閲覧して水をあげることができる一般公開と、リンクを知っている人だけが閲覧して水をあげることができる限定公開があります。そのため例に挙げた「カレーの辛さは何派?」のようなことだけではなく、飲み会の予定のようなプライベートなことにも利用できます。
一般公開はホーム画面に表示され、また検索の対象となります。限定公開はホーム画面に表示されず、検索の対象とならないため基本的にはリンクからしか遷移できません。

苗は3つまで植えることができます。アンケートであれば5つくらい苗を植えられる方が便利ですが、特にモバイルでの見た目が崩れてしまうため断念しました。見た目や雰囲気を重視した結果、私の力では3つが限界でした。

また、機能は制限されてしまいますがCookieを使用することで未登録状態でも利用できます。私の感覚ではよくわからないサービスに登録するのはかなりハードルの高いことなので、一旦少しでも触ってもらいたいなと思い実装しています。

このサービスで出来ること

細かい機能はいくつかありますがメイン部分はシンプルで最小限にしています。

  • 鉢を作って苗を植える
  • 水をあげる
  • 作った鉢を確認する
  • 水をあげた鉢を確認する

これだけです。

簡単に紹介させてください。

鉢を作って苗を植える

ヘッダーにある「Plant」ボタンを押下することでモーダルが開きます。

ここで鉢や苗の名前、タグを設定して「Let's Plant」ボタンを押下することで鉢を作って苗を植えることができます。右上のスイッチを切り替えることで一般公開するか、限定公開にするか切り替えることができます。ただし、一般公開はこのサービスに登録したユーザのみの機能です。未登録ユーザは自動的に限定公開として作成されます。

私が鉢を作るときに忘れてしまいがちなのですが、アイコンも変更することができます。

水をあげる

ホーム画面や検索結果の一覧あるいはリンクから鉢の画面を開きます。苗上部の水滴をクリックすることで水をあげることができます。水をあげた苗は少し成長して花が咲きます。

作った鉢を確認する

登録ユーザ限定の機能です。
ダッシュボードから確認することができます。

水をあげた鉢を確認する

同じく登録ユーザ限定の機能で、ダッシュボードから確認することができます。

実装備忘録

ここから個人的に次回の参考になりそうなことについていくつか残します。

Cloudflare Workersにデプロイできない事件

当時Cloudflare Workersにデプロイする予定で準備を進めていて、いざ作業に入るとエラーを吐いてデプロイできませんでした。

原因はCloudflare Workersで使っているEdge RuntimeNextAuthと互換性がないことです。解決するためにはNextAuthのバージョンを上げてv5にする必要があるとのことで、追加改修やセキュリティ的な観点的にCloudflare Workersへのデプロイを諦めました。
https://github.com/nextauthjs/next-auth/issues/7527

DBにユーザ情報を登録しない

このサービスではGoogleアカウントを使用してユーザ登録をするものの、Googleアカウントに紐づく名前やメールアドレスといった個人情報を登録していません。アンケートを作って投票する、ということに対して「誰が」という個人情報の必要性を感じなかったためです。
以下のようにNextAuthのサインイン処理でユーザ情報を削除しています。

export const authOptions: NextAuthOptions = {
  /*
    NextAuthで必要な設定
  */
  callbacks: {
    async signIn({ user }) {
      delete user.email;
      delete user.name;
      delete user.image;
      return true;
    },
  },
};

画像のキャッシュ利用

いくつも画像を使っているわけではないですが、毎度リクエストを投げているといくらお金がかかるかわからないのでキャッシュを利用するようにしています。
以下サイトの通りImageタグのsrcに画像パスを記載するのではなく、モジュールを設定してあげます。
https://zenn.dev/chot/articles/next-image-cache-control

レスポンスヘッダ設定

以下記事で説明されている内容にX-Content-Type-Optionsを加えて、以下例のようにしています。
https://zenn.dev/yutoo89/articles/d1ed72b821b2c0

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Strict-Transport-Security",
            value: "max-age=63072000; includeSubDomains; preload",
          },
          {
            key: "X-Frame-Options",
            value: "SAMEORIGIN",
          },
          {
            key: "Referrer-Policy",
            value: "strict-origin-when-cross-origin",
          },
          {
            key: "X-Content-Type-Options",
            value: "nosniff",
          },
        ],
      },
    ];
  },
};

export default nextConfig;

カードのリンク実装

以下のようなカードを実装しました。

カードをクリックした場合は鉢の画面へ、タグをクリックした場合はタグ検索をします。このように実装するにあたって、以下例のようにaタグを入れ子に実装して終わりのつもりだったのですがエラーになりました。どうやらaタグは入れ子にしたらダメみたいです。

<!-- このままではエラーが発生してしまう -->
<a href="/鉢画面への遷移用URL">
    <a href="/タグ検索用URL">タグ1</a>
    <a href="/タグ検索用URL">タグ2</a>
    <a href="/タグ検索用URL">タグ3</a>
</a>

そんな時にちょうど記事を書いてくださっている方がいて非常に助かりました。詳細は以下記事をご確認ください。一応Tailwind CSSで修正した例を残します。
https://zenn.dev/zozotech/articles/9e7b9059f15509

<div className="relative">
  <!-- カードのリンク領域をカード全体に広げる -->
  <Link href="/" className="absolute inset-0 z-0">カード全体のリンク</Link>
  <!-- タグのリンクをカードリンクの上に重ねる -->
  <Link href="/" className="relative z-10">タグのリンク</Link>
  <Link href="/" className="relative z-10">タグのリンク</Link>
  <Link href="/" className="relative z-10">タグのリンク</Link>
</article>

Xへの投稿機能

リンクのコピーだけではなくXについては直接ポストできるようにしています。あっさり実装できました。以下のように実装しています。

<Link
 target="_blank"
 rel="noopener"
 href={`https://twitter.com/intent/tweet?hashtags=bloomsurvey&text=新しく苗を植えました!%0A&url=${url}%0A`}
>

href属性を分解してみます。

  • https://twitter.com/intent/tweet:ベースのURLです。Xのポスト画面が立ち上がります。
  • hashtags=bloomsurvey:ハッシュタグを設定します。#bloomsurveyというハッシュタグを作成しています。
  • text=新しく苗を植えました!:テキストを挿入します。
  • %0A:改行を挿入します。
  • url=${url}:URLを設定します。ここでは鉢のリンクを設定しています。

実行するとこんな感じになります。

https://blog.stin.ink/articles/put-twitter-share-button

Cookie利用

記載済ですが未登録ユーザについてもサービスを利用できるようにCookieを使用しています。next/headersを使って以下のように実装しています(あくまでもイメージです)。

import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
  // パラメータとしてユーザIDをもらう体
  const params = await request.json();

  // Cookie確認
  const cookieStore = await cookies();
  // ユーザIDというkeyに対してCookieを設定する体
  let userId = cookieStore.get("ユーザID")?.value;
  // ユーザ登録されていなくてユーザID用Cookie存在しない場合、Cookieを発行
  if (!params.userId && !userId) {
    userId = crypto.randomUUID();
    cookieStore.set({
      name: "ユーザID",
      value: userId,
      path: "/",
      maxAge: 60 * 60 * 24 * 365,
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
    });
  }
}

フロント側では参照できない点に注意が必要です。

画像類のSEO対策

(私の調査不足な気がしますが)基本は調べて出てくる通りに設定すれば問題ないのですが1点だけ詰まりました。サービスアイコンが表示されない問題です。以下の通りicon.svgという名前でsrc/app/配下に配置することで解決しました。

以下、画像周りの配置例です。

src
  ├ public
  │   └ image
  │       └ ogp画像.png
  └ app
      ├ favicon.ico
      └ icon.svg

index or noindex

これもSEO対策関連です。このサービスでは一般公開と限定公開の2種類存在するため、限定公開の場合はインデックス登録されないようにしました。鉢にはどちらのアンケートかを管理するフラグを持っているので、それで以て制御しています。

以下のようなイメージです。

export async function generateMetadata() {
  const pot = await fetch('鉢データ取得');

  return {
    title: 'BloomSurvey',
    description: 'アンケートサービスです。',
    robots: {
        // index: falseの場合にnoindexになる
        index: !pot.linkOnly
      }
  };
}

Safari検証

検証版をリリースしていたので、先日iphoneユーザの同期に触ってもらったところ鉢の画面で恐ろしいくらいにスタイルがずれていました(画像はありませんが、鉢が水滴に重なっていて何票入っているかわからなくなっていました)。
調べたところabsolute周りで動作に違いが出ることがあるようです。対応としてはclassNamebottom-0を付けるだけの簡単なもので済みました。本番リリースはまだで、検証版だったのでよかったですがヒヤッとしました。やってないのは私くらいかもしれませんがChromeで開発している人はSafariも確認するようにしましょう。

余談

(調査不足かもしれませんが)過去にWindows版Safariなるものがあったようですが、セキュリティホールの問題があるため使用NGだそうです。そのためWindowsAndroidユーザの私はSafariの検証ができませんでした。最終的には直るであろうコードを書いてデプロイして、同期に見てもらいました。効率悪いですし、時間取ってしまって悪い気がしてしまいますし…良い方法があればご教授いただきたいです。

ビルド&デプロイ作業

思ったよりも時間がかかってしまったので、次回触るときに困らないようにまとめておきます。

まず全体像です。以降で各番号について触れていきます。

①ソースコードのPush

当たり前ですがコードを書いて動作確認を終えたらPushします。

②トリガーによる検知とビルド

Cloud Buildでトリガーを設定することで、例えば

  • 〇〇ブランチにpushされたとき
  • 〇〇ブランチにマージされたとき

のように任意のタイミングで自動でビルドすることができます。ビルドはDockerfileに従って行われます。
このトリガー設定はCloud Buildのページを直接開いて設定することは勿論できますが、Cloud Runのヘッダー部分にある「継続的デプロイを編集します」からCloud Buildのページを開いて設定することもできます。

余談

個人開発では規模が大きくないこともありテストコードを書いたことがなく、手動でゴリゴリ動かしています。個人開発で小規模とはいえテストコードを書くのが一般的なのでしょうか…?

③コンテナイメージのPush

ビルド結果に問題が無ければArtifact RegistryへコンテナイメージがPushされます。ビルド成功時に自動でPushされるので別途作業する必要はありません。ただし、トリガー設定時にイメージの場所と名前を設定する必要があります。

④デプロイ

Cloud Runで作成したイメージを使ってデプロイをします。Cloud Runヘッダー部の「新しいリビジョンの編集とデプロイ」からビルドしたコンテナイメージを選択してデプロイします。

うまくいかない場合

サービスがうまく立ち上がらなかったりAPIが実行できなかったりしました。原因と解決方法を記載します。

サービスが立ち上がらない

問題
デプロイしたのにもかからわらず「これはplaceholderです。」のようなメッセージが表示され、サービスが立ち上がっていない状態でした。
原因と対応
リビジョンで設定していたコンテナイメージが誤っていました。Artifact RegistryのどこにPushしているのかをCloud Buildで確認して、Cloud Runでそのコンテナイメージを設定できているか改めて確認する必要があります。

フロントエンドからAPIが叩けない

問題
サーバーサイドで実行されるAPIは問題なく動くのですが、ボタン押下等で実行されるAPIはエラーになりました。パスの一部がundefinedになっていることを確認しました。
原因と対応
通常、フロントエンドではNEXT_PUBLIC_〇〇という名前にすることで環境変数として有効になります。ただし、これがビルド時に必要になることに注意が必要です。

つまりCloud Runでは例のようにDockerflieで宣言する必要があるということです。バックエンドで使用される環境変数はリビジョンで設定されているままで問題ありません。

# APIのURL
ENV NEXT_PUBLIC_API_URL=APIのURLを設定

マネタイズ

広告を埋め込みたいですが、ある程度のアクセス数が無いと審査が通らないため今のところ取り込めていないです。サブスクに憧れはありますが、機能がシンプルで最小限のため課金していただいても提供するものがないのが現状です。ダッシュボード画面で「工事中」としていますが、正直なところ工事は始まっていません。

当面はGoogle AdSenseの審査に通ることを目標に今後も作業を続けてみます。

コスト

参考になるかわかりませんが、このサービスが止まらない限りは更新し続けるようにします。

費用
2月(開発のみ) 21円
3月(3月4日時点、開発のみ) 4円

振り返りと今後

ひとまず開発の目的としていたUIコンポーネントを使用せず開発・リリースができてよかったです。苦手なりにデザインに力を入れてみて、全体の雰囲気を統一できました。苗や花もTailwind CSSで作ることができて感動しました。

ただ、やはり開発するうえでデザイン面で何か勉強や改善が必要に感じました。イメージしたものを実装してみて、そこからちょこちょこ修正する方針で作業していました。ただ、そのイメージもいけてないことが多く練り直しが非常に多かったです。Figma等を使って先にデザインを固めるとだいぶ楽なんだろうなーと思いました。
次の開発に着手する前にFigmaに挑戦してみます。

工事中の機能がいくつかあるので、このサービスの改修もやっていきます。
例えばサブスクです。触ってみたい領域ではあったので、提供する機能が決まり次第挑戦したいです。
鉢や苗の編集もできないので追加実装します(これは早々に対応するようにします)。

その他の機能も実装してより良いサービスにしたいので、触っていただいて「こんな機能があったら」、「こんなデザインはどうだろう」など改善点やアイディアをいただけると非常に嬉しいです。
https://bloomsurvey.com/

どこか一部でも参考になれば幸いです。
最後までありがとうございました。

その他参考

記事の本文中で紹介していない参考記事等について紹介させてください。

こちらの記事を参考に読点の数を意識してみました。
https://zenn.dev/tmknom/articles/readable-github-cicd-book

リリース前チェックに使用させていただきました。
https://zenn.dev/catnose99/articles/547cbf57e5ad28

Discussion