開発TODO
Sankey Diagram(サンキーダイアグラム)の作成
財務データの流れを可視化するSankey Diagramを作成する。

参考イメージ
- 左側:収益の内訳(Subscription, Product, Services & other)
- 中央:Total Revenue → Gross Profit → Operating Income
- 右側:費用の内訳(Cost of revenue, S&M, R&D, G&A, Taxes, Interest expense)と Net Income
表示する指標
| 項目 | 例 |
|---|---|
| Total revenue | $6.0B, 9% Y/Y |
| Gross profit | $5.4B, 10% Y/Y |
| Operating income | $2.3B, 5% Y/Y |
| Net income | $1.8B, 6% Y/Y |
| Operating expenses | $3.2B, 10% Y/Y |
実装候補ライブラリ
- D3.js d3-sankey - 最も柔軟、カスタマイズ性高い
- Apache ECharts - sankey chart対応、設定が簡単
- Plotly.js - インタラクティブなsankey対応
タスク
- ライブラリ選定
- サンプルデータ構造の設計
- Vue コンポーネント作成
- 実際の財務データで表示
actual-consensus ページへのデータテーブル機能追加
既存の actual-consensus ページに nvidia-financial-chart のようなデータテーブル+チャート連動機能を追加する。
📎 対象ページ: https://log.eurekapu.com/financial-quiz/actual-consensus 📎 参考実装: https://log.eurekapu.com/financial-data/nvidia-financial-chart
実装する機能
- データテーブル表示
- チャート下部に全指標のデータテーブルを表示
- 四半期ごとの時系列データ
- 指標のチェックボックス選択
- チェックを付けた指標のみグラフに表示
- Metricsセクションで表示/非表示を切り替え
- 左軸・右軸の切り替え機能(重要)
- 左軸: 金額ベースの指標(売上、利益など)
- 右軸: マージン率・成長率
- 「L/R」ボタンで軸の割り当てを操作
タスク
- nvidia-financial-chart の実装を参考に設計
- データテーブルコンポーネント作成
- 指標選択UI(チェックボックス)実装
- 左軸/右軸切り替え機能実装
- actual-consensus ページに統合
FastAPI + htmx プロトタイプ開発(別プロジェクト)
AIスタートアップの初期フェーズ向けモック開発スタック。
📎 参考: https://zenn.dev/livetoon/articles/04dccf642d324c
なぜ FastAPI + htmx?
- Python統一: AI/LLM開発でPython必須の環境に最適
- セットアップ最小限:
pip install fastapi uvicorn jinja2のみ - 単一サーバー: CORS設定不要、管理が楽
- htmx: HTML属性だけでJS不要の非同期通信
技術スタック
| カテゴリ | 技術 |
|---|---|
| Backend | FastAPI |
| Frontend | htmx + Jinja2 |
| CSS | Tailwind CSS + DaisyUI |
| ORM | SQLModel |
htmx 実装パターン例
- 定期自動更新(
hx-trigger="every 5s") - フィルター検索
- SSEストリーミング対応
- 無限スクロール
SQLModel - DBモデルとAPIスキーマの統一
📎 参考: https://zenn.dev/livetoon/articles/9923c448c2734c
FastAPI作者が開発したORM。Pydantic + SQLAlchemy の融合。
革新的ポイント:
- 二重管理不要: 1つのクラスでDBモデルとAPIスキーマを兼用
table=TrueでDBモデル、外せばPydanticモデル- 型ヒントがそのままDBの型に変換
- Relationshipで親子関係を直感的に定義
- 非同期処理対応(aiosqlite)
from sqlmodel import SQLModel, Field
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
email: str = Field(index=True)
タスク
- 新規プロジェクト作成(別リポジトリ)
- FastAPI + htmx 環境セットアップ
- SQLModelでモデル定義
- 基本的なCRUDサンプル作成
- htmxパターン(自動更新、検索、SSE)を試す
Hono + htmx + Cloudflare スタック(別プロジェクト候補)
Hono作者 yusukebe 提唱のシンプル&高速スタック。
📎 参考: https://zenn.dev/yusukebe/articles/e8ff26c8507799
技術スタック
| カテゴリ | 技術 |
|---|---|
| Backend | Hono + JSX(サーバーサイド) |
| Frontend | htmx |
| Hosting | Cloudflare Workers(エッジ) |
| Database | Cloudflare D1(SQLite) |
| Validation | Zod |
| CSS | Tailwind CSS |
なぜ Hono + htmx?
- JSXをサーバーサイドテンプレートとして使用: ハイドレーション不要で高速
- エッジ実行: Cloudflare Workersで低レイテンシ
- 超軽量: TODOアプリが100行程度、Gzip時22KB
- PHP/Rails的アプローチの現代版: シンプルなSSR + htmxでSPA不要
FastAPI vs Hono 比較
| 観点 | FastAPI + htmx | Hono + htmx |
|---|---|---|
| 言語 | Python | TypeScript |
| 用途 | AI/LLM開発向け | エッジ・軽量アプリ向け |
| デプロイ | 任意のPythonホスト | Cloudflare Workers |
| テンプレート | Jinja2 | JSX |
タスク
- Hono + htmx のサンプル作成を検討
- D1との連携を試す
アニメーション動画のMP4変換バックエンド
現在のWebM出力をMP4に変換するバックエンド機能の検討。
現状の課題
- ブラウザ側でWebM生成 → MP4への変換が必要
- MP4の方が互換性が高い(iOS Safari、SNS共有など)
技術選定: FastAPI + FFmpeg
| 観点 | Python (FastAPI) | Hono (Cloudflare Workers) |
|---|---|---|
| FFmpeg実行 | ✅ 簡単(subprocess) | ❌ Workers不可 |
| 処理場所 | サーバー/VPS | エッジでは無理 |
| 実装の楽さ | ✅ 楽 | △ 外部サービス必要 |
結論: MP4変換には FastAPI + FFmpeg が最適
Cloudflare Workers はサーバーレス/エッジの制約でFFmpegを直接実行できない。 動画変換は重い処理のため、サーバーフル環境が必要。
推奨アーキテクチャ
┌─────────────────────────────────────────────────────────┐
│ フロントエンド: Nuxt (SSR/SSG) │
│ - SEO対応(企業ページは静的生成) │
│ - 財務データ閲覧は誰でも可能 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 動画ダウンロード機能(会員限定) │
│ - 会員登録 → ログイン必須 │
│ - WebM生成(ブラウザ)→ FastAPI に送信 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ バックエンド: FastAPI + FFmpeg │
│ - WebM → MP4 変換 │
│ - ホスト: Railway / Render / EC2 など │
└─────────────────────────────────────────────────────────┘
ビジネスモデル
| 機能 | アクセス |
|---|---|
| 財務データ閲覧 | 無料(SEO集客) |
| アニメーション再生 | 無料 |
| MP4動画ダウンロード | 会員登録必須 |
FastAPI 実装例
from fastapi import FastAPI, UploadFile
import subprocess
import tempfile
app = FastAPI()
@app.post("/api/convert-to-mp4")
async def convert_to_mp4(file: UploadFile):
with tempfile.NamedTemporaryFile(suffix=".webm") as input_file:
input_file.write(await file.read())
output_path = input_file.name.replace(".webm", ".mp4")
subprocess.run([
"ffmpeg", "-i", input_file.name,
"-c:v", "libx264", "-preset", "fast",
"-c:a", "aac",
output_path
])
return FileResponse(output_path, filename="animation.mp4")
重要: 静的MP4 vs 動的生成の判断
まず考えるべきこと: 本当にバックエンドが必要か?
パターン A: 静的MP4配信(バックエンド不要)⭐推奨
適用条件:
- データが固定(決算期ごとの更新のみ)
- ユーザーがカスタマイズしない
- 全ユーザーに同じ動画を提供
現在の財務データページ(QQQ構成銘柄等)はこれに該当!
❌ 非効率(毎回サーバーで生成)
ユーザーがボタン押す → WebM生成 → サーバーでMP4変換 → ダウンロード
(毎回同じ動画を生成...コストと時間の無駄!)
✅ 効率的(事前生成して静的配信)
事前にローカルでMP4生成 → /public/videos/ に配置 → ボタンでダウンロード
メリット:
- サーバーコスト $0(Cloudflare Pagesの静的配信のみ)
- ダウンロード 爆速(生成待ち時間なし)
- バックエンド 不要
運用フロー:
決算データ更新時(四半期ごと)
↓
ローカルでMP4生成(100社分をバッチ処理)
↓
/public/videos/GOOGL-2024.mp4 等に配置
↓
git push → Cloudflare Pagesにデプロイ
パターン B: 動的生成(バックエンド必要)
適用条件:
- ユーザーが期間を自由に選べる
- ユーザーが表示指標をカスタマイズできる
- リアルタイムデータを使用する
- ユーザー固有のデータで動画生成
この場合のみ Railway 等のバックエンドが必要
┌────────────────────────────────────────┐
│ Cloudflare Pages (Nuxt) │
│ - フロントエンド │
└────────────────────────────────────────┘
│
▼ API呼び出し
┌────────────────────────────────────────┐
│ Railway ($5/月) │
│ - FastAPI + FFmpeg │
│ - MP4変換処理 │
└────────────────────────────────────────┘
バックエンドホスティングの詳細: バックエンドホスティングサービス比較
現プロジェクトの結論
QQQ構成銘柄ページ → パターン A(静的MP4)を採用
- データは固定(決算期更新のみ)
- カスタマイズ機能なし
- バックエンド不要、コスト $0
プロジェクト構成(モノレポ)
フロントエンドとバックエンドを1つのリポジトリで管理し、それぞれ別のサービスにデプロイ。
パターン A: 新規プロジェクトの場合
financial-app/
├── README.md
├── .gitignore
│
├── frontend/ # Nuxt アプリ → Cloudflare Pages
│ ├── nuxt.config.ts
│ ├── package.json
│ ├── .env.example
│ ├── app/
│ │ ├── pages/
│ │ ├── components/
│ │ └── composables/
│ ├── content/ # MDXコンテンツ
│ └── public/
│
├── backend/ # FastAPI → Render / Railway
│ ├── main.py # FastAPIエントリポイント
│ ├── requirements.txt # Python依存関係
│ ├── Dockerfile # コンテナ化(任意)
│ ├── .env.example
│ ├── app/
│ │ ├── __init__.py
│ │ ├── api/
│ │ │ ├── __init__.py
│ │ │ ├── routes/
│ │ │ │ ├── convert.py # 動画変換API
│ │ │ │ └── auth.py # 認証API
│ │ │ └── deps.py # 依存性注入
│ │ ├── core/
│ │ │ ├── config.py # 設定
│ │ │ └── security.py # JWT等
│ │ └── services/
│ │ └── ffmpeg.py # FFmpeg処理
│ └── tests/
│
└── docs/ # 共有ドキュメント(任意)
パターン B: 現在のプロジェクト(mdx-playground)の場合
すでにモノレポ構成が整っている!
mdx-playground/
├── apps/
│ ├── web/ # フロントエンド(Nuxt)→ Cloudflare Pages
│ │ ├── nuxt.config.ts
│ │ ├── package.json
│ │ ├── app/
│ │ │ ├── pages/
│ │ │ ├── components/
│ │ │ └── composables/
│ │ └── content/
│ │
│ └── api/ # バックエンド(FastAPI)→ Render / Railway
│ ├── main.py # ← 追加
│ ├── requirements.txt # ← 追加
│ ├── app/ # ← 追加
│ │ └── services/
│ │ └── ffmpeg.py
│ └── README.md # 既存(空)
│
├── packages/ # 共有パッケージ(必要に応じて)
└── ...
対応表:
| 新規プロジェクト | 現在のプロジェクト | 役割 |
|---|---|---|
frontend/ | apps/web/ | Nuxt フロントエンド |
backend/ | apps/api/ | FastAPI バックエンド |
メリット: 別プロジェクト作成不要!
apps/api/ ディレクトリは現在ほぼ空(README.mdのみ)なので、
ここにFastAPIをセットアップすれば今すぐバックエンド追加可能。
現プロジェクトでのデプロイ設定
Cloudflare Pages(apps/web) - 既存設定そのまま
Render(apps/api) - 新規追加
Root Directory: apps/api
Build Command: pip install -r requirements.txt
Start Command: uvicorn main:app --host 0.0.0.0 --port $PORT
デプロイフロー
┌─────────────────────────────────────────────────────────┐
│ GitHub リポジトリ │
│ financial-app/ │
└─────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Cloudflare Pages │ │ Render / Railway │
│ (frontend/) │ │ (backend/) │
│ │ │ │
│ Root: /frontend │ │ Root: /backend │
│ Build: pnpm build │ │ Build: pip install │
│ Output: .output │ │ Start: uvicorn │
└──────────────────────┘ └──────────────────────┘
│ │
│ API呼び出し │
└────────────────────────────┘
各サービスの設定
Cloudflare Pages(frontend)
Build command: cd frontend && pnpm install && pnpm build
Build output directory: frontend/.output/public
Root directory: /
または Root directory を frontend に設定:
Root directory: frontend
Build command: pnpm install && pnpm build
Build output directory: .output/public
Render(backend)
# render.yaml(任意)
services:
- type: web
name: financial-api
env: python
rootDir: backend
buildCommand: pip install -r requirements.txt
startCommand: uvicorn main:app --host 0.0.0.0 --port $PORT
または手動設定:
Root Directory: backend
Build Command: pip install -r requirements.txt
Start Command: uvicorn main:app --host 0.0.0.0 --port $PORT
環境変数の繋ぎ込み
frontend/.env
# 開発時(ローカル)
NUXT_PUBLIC_API_URL=http://localhost:8000
# 本番(Render)
NUXT_PUBLIC_API_URL=https://financial-api.onrender.com
# 本番(Railway移行後)
NUXT_PUBLIC_API_URL=https://financial-api.up.railway.app
backend/.env
# CORS許可するフロントエンドのURL
FRONTEND_URL=https://your-app.pages.dev
# JWT認証用シークレット
SECRET_KEY=your-secret-key
# その他
DEBUG=false
backend/main.py(最小構成)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os
app = FastAPI()
# CORS設定(フロントエンドからのアクセスを許可)
app.add_middleware(
CORSMiddleware,
allow_origins=[os.getenv("FRONTEND_URL", "http://localhost:3000")],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
def health_check():
return {"status": "ok"}
@app.post("/api/convert-to-mp4")
async def convert_to_mp4(file: UploadFile):
# FFmpeg変換処理
pass
frontend での API 呼び出し
// frontend/app/composables/useApi.ts
export const useApi = () => {
const config = useRuntimeConfig()
const apiUrl = config.public.apiUrl // NUXT_PUBLIC_API_URL
const convertToMp4 = async (webmBlob: Blob) => {
const formData = new FormData()
formData.append('file', webmBlob, 'animation.webm')
const response = await fetch(`${apiUrl}/api/convert-to-mp4`, {
method: 'POST',
body: formData,
credentials: 'include', // 認証Cookie送信
})
return response.blob()
}
return { convertToMp4 }
}
タスク
- 新規リポジトリ作成(financial-app)
- frontend/ ディレクトリにNuxtセットアップ
- backend/ ディレクトリにFastAPIセットアップ
- FFmpeg 変換エンドポイント実装
- CORS設定
- 認証機能(会員登録/ログイン)
- Nuxt側の会員限定UI実装
- Render にバックエンドをデプロイ
- Cloudflare Pages にフロントエンドをデプロイ
- 環境変数で繋ぎ込み
- ユーザー増加時に Railway へ移行