この記事を読むと何ができるか
Zenn に投稿した技術記事を、Instagram Carousel 画像(PNG × 5〜8枚)と Reel / YouTube Shorts 用の短尺動画(MP4、タイピングアニメーション)にプログラムで自動変換できるようになる。
手動で Canva を開いてスライドを作って……という作業から完全に解放される。記事の構造データを JSON で定義すれば、npx tsx scripts/render-all.ts のワンコマンドで全コンテンツが生成される仕組みだ。
📋 前提条件
- Node.js 18 以上
- TypeScript の基本がわかる
- React の基本がわかる(コンポーネント、props あたり)
- Instagram / YouTube Shorts への投稿に興味がある
なぜ「記事→SNS」を自動化しようと思ったか
私は製造業で生産技術のエンジニアをしながら、副業で C# コンサルタント事業を準備している。技術記事を Zenn に書いてはいるが、Zenn だけでは認知の広がりに限界を感じていた。
Instagram や YouTube Shorts にもコンテンツを展開したい。でも、記事を書くだけで週5時間がギリギリの稼働時間の中で、Canva を開いてスライドを作って、動画も編集して……という手作業を毎回やるのは現実的じゃない。
「記事の内容を JSON で定義すれば、画像も動画も全部自動で生成される仕組みがほしい」
これが出発点だった。
技術選定 — なぜ Remotion を選んだか
SNS 用の画像・動画を自動生成する方法はいくつかある。私が検討した選択肢と、Remotion を選んだ理由をまとめた。
| 選択肢 | 画像生成 | 動画生成 | プログラマブル | 学習コスト | 判定 |
|---|---|---|---|---|---|
| Canva | ✅ | ✅ | ❌ 手作業 | 低 | ❌ 自動化できない |
| FFmpeg + ImageMagick | ✅ | ✅ | ✅ | 高(コマンドライン地獄) | △ 保守性に難 |
| Puppeteer + HTML | ✅ | △ | ✅ | 中 | △ 動画が弱い |
| Remotion(React) | ✅ | ✅ | ✅ | 中(React知識が活きる) | ✅ 採用 |
Remotion を選んだ決め手は3つ:
- React でスライドを書ける — HTML/CSS の表現力がそのまま使える。「コードブロックにシンタックスハイライトをつけたい」も React コンポーネントで解決
-
静止画(
renderStill)も動画(renderMedia)も同じ仕組み — Carousel 画像と Reel 動画を同じプロジェクトで管理できる - GitHub Copilot との相性がいい — React コンポーネントなので、AI にスライドのデザイン調整を依頼しやすい
正直に言うと、Remotion は最初の環境構築で Chrome Headless Shell のダウンロード(約108MB)が走るので「えっ、こんなに重いの?」とは思った。でも2回目以降はキャッシュされるので気にならない。
全体のアーキテクチャ
システムの全体像はシンプルだ:
Zenn記事 (Markdown)
↓ AI(Copilot CLI)が分析・要約
ArticleData JSON
↓ Remotion レンダリング
├── carousel/slide-01.png 〜 slide-07.png(Carousel 画像)
├── reel/reel-1080x1920.mp4(Reel / Shorts)
├── reel/reel-720x1280.mp4(X/Twitter 用)
└── caption-instagram.txt / caption-x.txt / caption-youtube.txt
ポイントは JSON → 画像/動画 の変換を完全にプログラマブルにした こと。記事の内容が変われば JSON を差し替えるだけで、同じパイプラインからコンテンツが生成される。
Remotion プロジェクトのセットアップ
プロジェクト初期化
# プロジェクトディレクトリを作成
mkdir jodycraft-social && cd jodycraft-social
# 必要なパッケージをインストール
npm init -y
npm install remotion @remotion/bundler @remotion/cli @remotion/renderer react react-dom
npm install -D typescript tsx @types/react @types/react-dom
ディレクトリ構成
jodycraft-social/
├── src/
│ ├── Root.tsx # Remotion エントリーポイント
│ ├── types.ts # 型定義
│ ├── components/
│ │ ├── carousel/ # Carousel スライドコンポーネント
│ │ │ ├── TitleSlide.tsx
│ │ │ ├── ContentSlide.tsx
│ │ │ ├── CodeSnippetSlide.tsx
│ │ │ ├── SummarySlide.tsx
│ │ │ └── CtaSlide.tsx
│ │ └── reel/ # Reel シーンコンポーネント
│ │ ├── TitleScene.tsx
│ │ ├── CodeTypingScene.tsx
│ │ ├── PointScene.tsx
│ │ └── CtaScene.tsx
│ └── compositions/
│ ├── CarouselSlide.tsx # Carousel 用 Composition
│ └── CodeReel.tsx # Reel 用 Composition
├── scripts/
│ ├── render-carousel.ts # Carousel レンダリングスクリプト
│ ├── render-reel.ts # Reel レンダリングスクリプト
│ └── render-all.ts # 一括レンダリングスクリプト
└── out/ # 生成物の出力先
Instagram Carousel — React コンポーネントで画像を作る
データ構造(CarouselData)
まず、Carousel の入力データの型を定義する:
// src/types.ts
export interface SlideData {
type: 'title' | 'content' | 'code' | 'summary' | 'cta';
title?: string;
subtitle?: string;
emoji?: string;
bullets?: string[];
body?: string;
code?: string;
codeLanguage?: string;
codeCaption?: string;
}
export interface CarouselData {
articleTitle: string;
emoji: string;
topics: string[];
slides: SlideData[];
caption: string;
hashtags: string[];
}
各スライドは type で種類を区別する。Remotion はこの type に基づいて、対応する React コンポーネントをレンダリングする。
コードスニペットスライドの実装例
5種類のスライドの中で、最も「技術記事っぽさ」が出るのがコードスニペットスライド。GitHub Dark テーマ風のデザインにした:
// src/components/carousel/CodeSnippetSlide.tsx
import React from 'react';
export const CodeSnippetSlide: React.FC<{
title: string;
code: string;
codeLanguage: string;
codeCaption?: string;
}> = ({ title, code, codeLanguage, codeCaption }) => {
return (
<div style={{
width: 1080, height: 1080,
background: 'linear-gradient(135deg, #0F172A 0%, #1E293B 100%)',
display: 'flex', flexDirection: 'column',
padding: 60,
fontFamily: 'Noto Sans JP, sans-serif',
}}>
{/* タイトル */}
<h2 style={{ color: '#E2E8F0', fontSize: 42, marginBottom: 30 }}>
{title}
</h2>
{/* コードブロック(GitHub Dark風) */}
<div style={{
background: '#0D1117',
borderRadius: 16,
padding: '30px 35px',
flex: 1,
border: '1px solid #30363D',
}}>
{/* 言語タグ */}
<div style={{
color: '#8B949E', fontSize: 18,
marginBottom: 20, fontFamily: 'monospace',
}}>
{codeLanguage}
</div>
{/* コード本体 */}
<pre style={{
color: '#E6EDF3', fontSize: 24,
fontFamily: "'Cascadia Code', 'Fira Code', monospace",
lineHeight: 1.6, margin: 0, whiteSpace: 'pre-wrap',
}}>
{code}
</pre>
</div>
{/* キャプション */}
{codeCaption && (
<p style={{ color: '#94A3B8', fontSize: 26, marginTop: 25 }}>
💡 {codeCaption}
</p>
)}
</div>
);
};
ここ、ちょっと面白いポイントがあって、Remotion のレンダリングは内部で Chrome Headless Shell を使うので、CSS の表現力がそのまま活きる。linear-gradient でグラデーション背景にしたり、border-radius で角丸にしたり、Web で表現できることはほぼ全部できる。
Composition の定義
Remotion では Composition(コンポジション)を定義して、レンダリング対象を宣言する。Carousel の場合は1枚のスライドが1つの Composition:
// src/compositions/CarouselSlide.tsx
import { Composition, getInputProps } from 'remotion';
export const CarouselSlide: React.FC = () => {
const props = getInputProps() as { slide: SlideData };
const { slide } = props;
switch (slide.type) {
case 'title':
return <TitleSlide {...slide} />;
case 'content':
return <ContentSlide {...slide} />;
case 'code':
return <CodeSnippetSlide {...slide} />;
case 'summary':
return <SummarySlide {...slide} />;
case 'cta':
return <CtaSlide {...slide} />;
}
};
レンダリングスクリプト — ハマりポイントあり
ここが一番ハマった。Remotion v4 の renderStill() でスライドごとに異なる inputProps を渡す場合、selectComposition() を毎回呼ぶ必要がある。
// scripts/render-carousel.ts(核心部分)
import { bundle } from '@remotion/bundler';
import { renderStill, selectComposition } from '@remotion/renderer';
// 1. バンドルを作成
const serveUrl = await bundle({ entryPoint: './src/index.ts' });
// 2. 各スライドをレンダリング
for (let i = 0; i < slides.length; i++) {
const inputProps = { slide: slides[i] };
// ⚠️ ここが重要: スライドごとに selectComposition を呼ぶ
const composition = await selectComposition({
serveUrl,
id: 'CarouselSlide',
inputProps, // ← ここで inputProps を渡す
});
await renderStill({
composition, // selectComposition の結果を使う
serveUrl,
output: `out/carousel/slide-${String(i + 1).padStart(2, '0')}.png`,
});
}
最初にやった間違い: getCompositions() で全 Composition を取得して、renderStill({ composition, inputProps }) で props を渡す方法を試した。しかしこれだと 全スライドが同じ内容(デフォルト props)でレンダリングされてしまう。
Remotion v4 では selectComposition() に inputProps を渡して、Composition ごとに props を確定させる仕組みに変わっている。公式ドキュメントにも書いてあるが、見落としやすいポイントだった。
Reel 動画 — タイピングアニメーションを作る
Reel / YouTube Shorts 用の縦長動画(1080×1920, 30fps)を生成する。技術記事のコードが1文字ずつタイピングされていくアニメーションは、SNS でけっこう目を引く。
タイピングアニメーションの仕組み
Remotion の useCurrentFrame() を使って、フレーム番号に応じて表示する文字数を計算する。仕組みは単純:
// src/components/reel/CodeTypingScene.tsx
import { useCurrentFrame, useVideoConfig } from 'remotion';
export const CodeTypingScene: React.FC<{
code: string;
codeLanguage: string;
startFrame: number;
}> = ({ code, codeLanguage, startFrame }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const elapsed = frame - startFrame;
const charsPerFrame = 2; // 30fps で1秒に60文字
const charsToShow = Math.max(0, Math.floor(elapsed * charsPerFrame));
const visibleCode = code.slice(0, charsToShow);
// カーソルの点滅(0.5秒周期)
const cursorVisible = Math.floor(frame / (fps / 2)) % 2 === 0;
return (
<div style={{ /* 背景スタイル省略 */ }}>
<pre style={{
color: '#E6EDF3',
fontFamily: "'Cascadia Code', monospace",
fontSize: 28,
lineHeight: 1.5,
}}>
{visibleCode}
{cursorVisible && <span style={{ color: '#58A6FF' }}>▌</span>}
</pre>
</div>
);
};
charsPerFrame = 2 で、30fps なら 1秒に60文字 のタイピング速度になる。コードが 600文字なら約10秒で打ち終わる計算。実際に再生してみて、速すぎたり遅すぎたりしたら調整すればいい。
複数シーンの統合
Reel は「タイトル→コードタイピング→ポイント→CTA」の4シーン構成。各シーンの秒数を durationInSeconds で指定して、フレーム数に変換する:
// 各シーンのフレーム数を計算
const fps = 30;
const scenes = reelData.scenes;
let totalFrames = 0;
for (const scene of scenes) {
scene._startFrame = totalFrames;
scene._durationFrames = scene.durationInSeconds * fps;
totalFrames += scene._durationFrames;
}
// → 例: title(3秒=90f) + code(22秒=660f) + point(6秒=180f) + cta(3秒=90f)
// → 合計 1020フレーム = 34秒
マルチ解像度出力
Instagram Reel と YouTube Shorts は 1080×1920(9:16)だが、X/Twitter は 720×1280 が推奨。renderMedia() で Composition のサイズをオーバーライドすれば、同じコンポーネントから複数解像度を出力できる:
// 1080x1920(Instagram Reel / YouTube Shorts)
const composition1080 = await selectComposition({ serveUrl, id: 'CodeReel', inputProps });
await renderMedia({
composition: composition1080,
serveUrl,
codec: 'h264',
outputLocation: 'out/reel/reel-1080x1920.mp4',
});
// 720x1280(X/Twitter 用)— Composition サイズをオーバーライド
const composition720 = await selectComposition({ serveUrl, id: 'CodeReel', inputProps });
await renderMedia({
composition: { ...composition720, width: 720, height: 1280 },
serveUrl,
codec: 'h264',
outputLocation: 'out/reel/reel-720x1280.mp4',
});
一括生成スクリプト — ワンコマンドで全部出す
記事1本から Carousel + Reel + キャプション全部を生成する統合スクリプトを作った:
npx tsx scripts/render-all.ts \
--input out/visa-article/article-data.json \
--outDir out/visa-article
これで以下が一発で生成される:
out/visa-article/
├── carousel/
│ ├── slide-01.png 〜 slide-07.png ← Carousel 画像 7枚
├── reel/
│ ├── reel-1080x1920.mp4 ← Reel / Shorts (34秒, 2.1MB)
│ └── reel-720x1280.mp4 ← X用 (34秒, 2.1MB)
├── caption-instagram.txt ← ハッシュタグ付きキャプション
├── caption-x.txt ← 280文字以内 + リンク
└── caption-youtube.txt ← タイトル + 説明 + タグ
所要時間はマシンスペックにもよるが、私の環境では 7枚のCarousel + 2本の動画で約2分。初回だけ Chrome Headless Shell のダウンロードに数分かかる。
ハマったところ・意外だったこと
1. ProtocolError: Target closed
レンダリング中に ProtocolError: Target closed というエラーが複数回出た。正直焦ったが、出力ファイルは正常に生成されていた。調べてみると、Remotion が内部的にブラウザタブを開閉する際に発生するもので、レンダリング結果には影響しないとのこと。無視して問題ない。
2. 日本語フォントの問題
Remotion は Chrome Headless Shell でレンダリングするため、システムにインストールされたフォントがそのまま使われる。Windows なら問題ないが、CI/CD 環境(Linux)で動かす場合は Noto Sans JP のインストールが必要になるだろう。
3. コードブロックの改行
JSON の中にコードを書くとき、改行は \n で表現する。ここは当たり前のようで、実際にやってみると「このバックスラッシュ、エスケープ必要だっけ?」と混乱する。TypeScript の JSON ファイルなら素直に \n でOK。
4. Composition の inputProps が反映されない
前述した selectComposition() vs getCompositions() の問題。これは2時間ロスした。Remotion v4 を使うなら selectComposition() + inputProps のパターンを徹底すること。
やってみてわかったこと
「プログラマブルな画像生成」は想像以上に快適
Canva のような GUI ツールだと、「全スライドのフォントサイズを変えたい」とか「背景色を統一したい」という変更が地味に面倒。でも React コンポーネントなら CSS を1箇所変えるだけで全スライドに反映される。
これは 技術記事のように同じフォーマットで量産するコンテンツと相性がいい。記事の内容は毎回変わるが、デザインのフレームは固定。まさにテンプレートの力が活きる場面だ。
AI × Remotion の可能性
今回のパイプラインで一番面白いのは、記事の分析から JSON 生成までを AI(GitHub Copilot CLI)に任せていること。Agent Skill(SKILL.md)に「記事を読んで、スライド構成を JSON で出力して」というルールを書いておくと、Copilot が記事を解析して適切なスライド構成を提案してくれる。
つまり、「記事を書いたら、SNS コンテンツ生成の指示を出すだけ」で全自動化できる。ここの詳細は別記事で書く予定。
動画のファイルサイズが小さい
タイピングアニメーションは画面の変化が少ないため、H.264 の圧縮がよく効く。34秒の Reel が約2MB。Instagram の動画アップロード上限(100MB)を気にする必要はまったくない。
まとめ
- Remotion は React でプログラマブルに画像・動画を生成できるフレームワーク
- Instagram Carousel (1080×1080 PNG) も Reel (1080×1920 MP4) も 同じ React プロジェクト で管理できる
-
selectComposition()+inputPropsのパターンで、スライドごとに異なるデータを渡す - タイピングアニメーションは
useCurrentFrame()を使うと簡単に実装できる - マルチ解像度出力は Composition のサイズオーバーライドで対応
- ワンコマンドで Carousel 7枚 + Reel 2本 + キャプション3ファイルを約2分で生成
次の記事では、この仕組みで生成したコンテンツを Buffer GraphQL API を使って Instagram / X / YouTube Shorts に自動投稿するパイプライン について書く予定だ。
📌 この記事が役に立ったら「いいね」してもらえると嬉しいです。SNS運用の自動化シリーズとして記事を書いていく予定なので、フォローもぜひ!
よくある質問
Q. Remotion は無料で使えますか?
Remotion 自体はオープンソース(Business Source License)で、個人利用や小規模プロジェクトなら無料です。企業での大規模利用にはライセンスが必要な場合があります。詳細は Remotion公式のライセンスページ を確認してください。
Q. Canva の方が手軽では?
単発で数枚のスライドを作るなら Canva の方が早いです。でも「毎週の記事からSNSコンテンツを量産する」なら、プログラマブルな Remotion の方が圧倒的に効率がいい。テンプレートを一度作れば、JSON を差し替えるだけで新しいコンテンツが生まれます。
Q. React を知らなくても使えますか?
基本的なコンポーネント(JSX の書き方、props の受け渡し)がわかれば十分です。複雑な状態管理やフックは使わない。CSS の知識の方が重要かもしれません。
Q. CI/CD で自動生成できますか?
技術的には可能です。GitHub Actions で Node.js 環境を用意し、Noto Sans JP フォントをインストールすれば動くはず。ただし Chrome Headless Shell のダウンロードがあるため、初回のビルド時間は長めになります。
Q. 動画にBGMを付けたい場合は?
Remotion は <Audio> コンポーネントで音声の合成もサポートしています。フリーのBGM素材をプロジェクトに配置して、Reel コンポーネントに <Audio src={bgm} /> を追加するだけです。
📝 この記事は Zenn で最初に公開されました。
最新版はZennをご覧ください。