1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude APIで毎晩CSSアニメーションが増えていくサイトを作った

1
Posted at

1 はじめに

今回は「n8n+Claude API」で、毎晩0時にCSSアニメーションを1つ自動生成して、増え続けていく日替わりCSSアニメーションサイトを作りました。

日替わりアニメーションサイト:https://saku0875.github.io/animation-gallery/
image.png

最近、CSSアニメーションを勉強していて、実装できるアニメーションの幅を広げたいなと思っており、そこで「AIに毎日CSSアニメーションを生成させて、そのコードを見て参考にできたら面白いのでは?」と思いついたため、このようなテーマになりました。

前回の記事で、n8nをXserverでセルフホストしたので、それを活用してサイトを作ってみました。
毎日0時にAIがテーマを考え、それに合わせてCSSアニメーションを自動で作成してくれるという仕組みになっています。
今回はn8nで動かしているワークフローとサイトの話をしていきます。

2 仕組みの全体

[n8n (VPS上で毎晩0時に起動)]
    │
    ├─ ① 日付・季節を判定、今日のモードを決める
    ├─ ② Claude API: 今日のテーマを考える
    ├─ ③ Claude API: テーマから作品を生成する
    └─ ④ GitHubリポジトリの animations.json に1件追記してコミット
             │
             ▼
[GitHub Pages] ← コミットを検知して自動反映
    │
    ▼
[ギャラリーサイト] JSONを読んで描画するだけの静的サイト
  • AIを二段構えにする
     「テーマを考える」と「テーマから作品を生成する」という2つの工程をAIに任せることで、事前に用意したテーマの配列から日替わりで選ぶという方法よりもテーマの幅を広げることが出来ています。
  • データベースを使っていない
     保存先はGitHubリポジトリ内のJSONファイル1つに絞っています。この構成により、コミットをトリガーにPagesが自動更新されるので、デプロイしなくてもサイトが更新されます。

3 n8nワークフロー

 ノードは全部で9個です。

Schedule Trigger → SetTheme → ThemeGen → ThemeParse
→ ArtGen → ParseResult → Get a file → 追記Code → Edit a file

image.png

全体的な流れとしては、「いつ動くか→お題を決める→作品を作る→GitHubに保存する」という4工程に分かれています。1つずつ見ていきます。

(1) Schedule Trigger ー 毎晩0時に起動する

ワークフローの起点です。Cron式でスケジュールを指定します。

  • Trigger Interval:Cron
  • Expression:0 0 * * *(毎日0時)

Xserverにn8nを立てる際に、docker-compose.ymlGENERIC_TIMEZONE=Asia/Tokyoを設定しました。これを設定していないとCronがUTC基準になり、毎日0時に動くはずのn8nが日本時間の9時に動いてしまいます。

(2) SetTheme(Codeノード) ー 今日のモードを決める

日付・季節を判定し、25%の確率で「季節モード」になるように設定しています。CSSアニメーションのテーマとして、毎回ランダムでは味気ないので、時々季節感に合うアニメーションを生成してもらえるように「基本はランダム、時々季節感のあるお題が出る」というバランスにしています。

const today = new Date();
const m = today.getMonth() + 1;
const season = m<=2||m===12 ? "冬" : m<=5 ? "春" : m<=8 ? "夏" : "秋";

// 25%の確率で「季節モード」、残り75%は「なんでもアリ」
const isSeasonal = Math.random() < 0.25;

return [{
  json: {
    date: today.toISOString().slice(0, 10),
    month: m,
    season: season,
    mode: isSeasonal ? "seasonal" : "free"
  }
}];

AIに「たまに季節感のあるアニメーションにして」というプロンプトで制御するよりも、n8nでコードによってモードを確定させてからテーマを決めさせる方が、狙った通りの頻度で動くので、このような構成にしています。

(3) ThemeGen(HTTP Requestノード) ー AIがお題を考える

Claude APIを呼んで、今日のお題を1単語だけ考えさせます。

  • Method:POST
  • URL:https://api.anthropic.com/v1/messages
  • Authentication:Generic Credential Type
  • Generic Auth Type:Header Auth
  • Header Auth:Header Auth account
  • Headers
     Name |anthropic-version
      Value |2023-06-01
     Name |content-type
      Value |application/json
{
  "model": "claude-haiku-4-5-20251001",
  "max_tokens": 50,
  "system": "あなたはCSSアニメーションのお題を考える人です。返答はお題1つだけ。「本」「雨」「花」「風船」のような、日常にある具体的なモノや自然現象を表すシンプルな単語(1〜4文字程度)にすること。説明・記号・引用符は一切不要。抽象概念や複合語は避ける。",
  "messages": [
    { "role": "user", "content": "={{ $json.mode === 'seasonal' ? `今は${$json.month}月(${$json.season})です。この季節の風物詩を、シンプルな単語1つで挙げてください。` : `CSSアニメーションのお題を、シンプルな単語1つで挙げてください。季節とは無関係で構いません。` }}" }
  ]
}

お題を考えるだけの軽いタスクなので、モデルは安価なHaikuを使います。

(4) ThemeParse(Codeノード) ー お題を取り出す

Claude APIの応答はcontent配列に入って返ってくるので、そこからテーマ文字列を取り出し、setThemeの日付と合流させます。

// ThemeGenが返したテーマ文字列を取り出す
const theme = $json.content.map(b => b.text || "").join("").trim();
const date = $('SetTheme').first().json.date;
return [{ json: { theme, date } }];
  • $('SetTheme')は「SetThemeという名前のノード出力を参照する」という意味
     ノード名と一致していないとhasn't been executedエラーが起こるので、参照するノードは事前に分かりやすい名前に変えておきましょう。

(5) ArtGen(HTTP Requestノード) ー お題から作品を生成する

本命のノードです。

  • Method:POST
  • URL:https://api.anthropic.com/v1/messages
  • Authentication:Generic Credential Type
  • Generic Auth Type:Header Auth
  • Header Auth:Header Auth account
  • Headers
     Name |anthropic-version
      Value |2023-06-01
     Name |content-type
      Value |application/json
{
  "model": "claude-sonnet-4-6",
  "max_tokens": 3000,
  "system": "あなたは余白と間を活かした上質なCSSアニメーションを作る作家です。応答はJSONのみ。コードフェンスや説明文は一切出力しないこと。形式: {\"title\": \"作品名(日本語)\", \"keyframes_css\": \"完結したCSS\", \"demo_html\": \"最小限のHTML\"}。\n\n【土台】.demo-stage に width:200px; height:200px; position:relative; overflow:hidden; を必ず明記。背景は #f7f6f2〜#ececea の薄グレー系(純白禁止)。\n\n【配色】グレー階調+やわらかいパステル2〜3色。背景とはっきり区別できる濃さにする(例: #e8a8b8, #90b8d8, #a0c8a0, #d8b878)。ビビッド・ネオン・原色・黒背景は禁止。\n\n【構図】テーマを1つの小さな情景として描く。主役のモチーフ(60px以上)を丁寧に造形し、それを引き立てる副要素を配置する。要素は3〜10個。::before/::after、border-radius、gradient、box-shadowを駆使して形を作り込む。blurは2px以下に抑える。\n\n【モーション】keyframesは2〜4個。主役と副要素で速度・タイミングを変え、nth-childのanimation-delayで時間差(スタッガー)を作る。ease-in-outやcubic-bezierで緩急をつけ、ループ(2〜6秒)の始点と終点が滑らかに繋がるようにする。\n\n【静止時の見栄え】重要: アニメーションの0%時点でも1枚の絵として成立させること。全要素をopacity:0から始めない。止め絵で見ても何のテーマか伝わる構図にする。\n\n【禁止】外部画像・JS。セレクタは必ず .demo-stage 配下にスコープ。アニメーションはinfinite。",
  "messages": [
    {
      "role": "user",
      "content": "今日のテーマ:「{{ $json.theme }}」。このテーマでCSSアニメーションを1つ作ってください。"
    }
  ]
}
  • {{ $json.theme }}に、直前のThemeParseが取り出したお題が当てはまる。

(6) ParseResult(Codeノード) ー 作品JSONをパースする

ArtGenの応答テキストをJSONとしてパースし、テーマや日付と合体させて保存用の1件分のデータを完成させます。

パース:プログラムが使いやすいカタチに変換する技術

// Claudeの応答テキストを取り出す
const raw = $json.content.map(b => b.text || "").join("");

// 万一コードフェンスが付いていても除去(保険)
const clean = raw.replace(/```json/g, "").replace(/```/g, "").trim();

// JSONとしてパース
const parsed = JSON.parse(clean);

// 前のCodeノードからtheme/dateを引っ張る
const meta = $('ThemeParse').first().json;

return [{
  json: {
    date: meta.date,
    theme: meta.theme,
    title: parsed.title,
    keyframes_css: parsed.keyframes_css,
    demo_html: parsed.demo_html
  }
}];

(7) Get a file(GitHubノード) ー 現在のJSONを取得する

リポジトリにあるanimations.jsonの中身を取得する。

  • Resource:File(Repositoryではない)
  • Operation:Get
  • Repository Owner/Name:自分のユーザー名/リポジトリ名
  • File Path:data/animations.json
  • As Binary Property:オフ

対象ファイルがないと404エラーになります。リポジトリにdata/animations.jsonを作り、中身[](空配列)で先にコミットしておいてください!

(8) 追記Code(Codeノード) ー 今日の1件を配列に追記する

先ほどGet a fileが返した中身をデコード→配列の先頭に今日の作品を追加→新しいJSON文字列を作ります。

// Get a file が返した Base64 の中身をデコード
const decoded = Buffer.from($json.content, 'base64').toString('utf-8');
const existing = JSON.parse(decoded);

// ParseResultの結果(今日の1件)を取得
const item = $('ParseResult').first().json;

// 先頭に追加(新しい順に並ぶ)
existing.unshift({
  date: item.date,
  theme: item.theme,
  title: item.title,
  keyframes_css: item.keyframes_css,
  demo_html: item.demo_html
});

return [{
  json: {
    newContent: JSON.stringify(existing, null, 2),
    commitMessage: `Add animation: ${item.title} (${item.date})`
  }
}];

先頭に追加しているのは、サイト側が「配列の先頭=最新」として扱うためです。

(9) Edit a file(GitHubノード) ー コミットして完了

最後に、新しい中身でファイルを上書きコミットします。

  • Resource:File
  • Operation:Edit
  • Repository Owner/Name:自分のユーザー名/リポジトリ名
  • File Path:data/animations.json
  • File Content(Expression):{{ $json.newContent }}
  • Commit Message(Expression):{{ $json.commitMessage }}

これによりコミット履歴には毎晩このようなログが刻まれます。

Add animation: 波のゆらぎ (2026-07-16)
Add animation: 蝉の鳴き声 (2026-07-15)

GitHub Pagesがコミットを検知して自動反映するので、デプロイ作業はしなくて済みます。

仕上げ ー ワークフローの有効化

全ノードが通ることを確認したら、右上の「Publish」でワークフローを有効化します。
これを忘れると、このワークフローが動かないため、毎晩0時になってもサイトが更新されないので、忘れずに有効化しておきましょう!

4 サイト側の実装

サイトはindex.html1枚の静的サイトにして、JSONを読んで描画するだけという仕組みになっています。

4-1 工夫

(1) アーカイブは停止して、ホバーで再生するようにする

作品数が3桁を超えてくると、同時に無限ループしたらブラウザが重たくなってしまうので、animation-play-state: pausedで止め絵にして、ホバーやタップで再生する設計にしています。

(2) コード ⇄ プレビュー切り替え

各カードの右上に</>ボタンを付けて、その作品のCSSとHTMLをその場で読めるようにしています。
AIのコードを眺めたり、参考にできるようになっています。

4-2 プロンプトに苦戦

今回のサイト作りにおいて、特に苦戦したところはAIにアニメーションを生成させるプロンプトです。
今のプロンプトに落ち着くまでに3回の試行がありました。

(1) 自由に書かせる

最初は「テーマに合わせてCSSアニメーションを書いて」というシンプルな指示でした。このプロンプトだと、作品は出るのですが、色がカラフルの時もあれば、黒背景だったり、白がメインになって背景と同化するため見にくいなど、様々な問題がありました。

(2) 制約を増やす

「背景は白、パステル、要素は1~4個まで」と制約を設けました。すると、今度は作品がシンプルになりすぎて、物足りないことが多くなりました。制約をかけすぎると、表現の幅を狭めてしまい、テーマを表現できないという問題が発生します。

(3) 方向づけをする

  • 止め絵で成立させる
     アニメーション0%の時点でも1枚の絵として成立するように指示しました。こうすることによって、アニメーションが動いていない時でも止め絵で見ても何のテーマか分かるようになりました。
  • 主役を作らせる
     要素は1~4個までという数の制約ではなく、主役を中心にそれを引き立てる副要素を配置するように指示しました。これにより、同じテーマでも表現力があがりました。

5 今後の展望

このような流れでワークフローを構築し、サイトの自動更新ができるようになりました!
まだ完成とは言えない出来栄えなので、これからも少しずつ更新していきたいと思っています。
今後のタスクとしては
① アニメーションを生成するためのプロンプトの見直し
 Claudeが生成してくれるCSSアニメーションの質をまだ上げられると思うので、プロンプトの試行錯誤をする
② フロントの改良
 これからさらにアニメーションが増えていくとカテゴリ分けだけでは見づらくなると思うので、検索機能を実装したり、期間を指定するなどしていく

以上となります!

n8n実際に使ってみてめっちゃ楽しかったです!
余談ですが、ワークフローによって毎日コミットするようになっているので毎日草が生えるようになりました
なんだか嘘ついてる気分になりますね()
image.png

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?