記事一覧に戻る
自動化

1つのMarkdownから4つの顔を持つ記事を自動生成する仕組みを作った話

同じ記事でもNote・Zenn・Qiita・ブログで読者層が違う。1つのMarkdownから各プラットフォームに最適化されたコンテンツを自動変換する仕組みを構築した全記録。

最終更新:

1つのMarkdownから4つの顔を持つ記事を自動生成する仕組みを作った話


🎯 この記事で得られること

この記事を読むと、以下のことができるようになります。

  • ✅ 1つのMarkdownから Note・Zenn・Qiita・ブログ向けに自動変換する仕組みの設計
  • ✅ 見出しキーワード判定による「セクション単位の自動除去」アルゴリズム
  • ✅ プラットフォームごとの読者層に合わせたコンテンツ最適化戦略
  • ✅ canonical URLによるSEO重複回避の実装方法

😰 あなたもこんな悩みを抱えていませんか?

  • 「Note・Zenn・Qiita・ブログに同じ記事を投稿したいけど、毎回手作業で書き直すのが面倒…」
  • 「Noteにコードブロックを載せても読まれないし、Zennにポエムを書いても刺さらない…」
  • 「結局、ブログだけに投稿して他のプラットフォームは放置してしまっている…」

私も1週間前まで、まったく同じ状態でした。

マルチプラットフォーム投稿ツールを作ったものの、全プラットフォームに同じ内容をそのまま投稿していました。技術記事をNoteに投稿すればコードが邪魔になり、ストーリー重視の記事をZennに投稿すれば「ポエムかよ」と言われる。

「同じ記事なのに、投稿先によって最適な形が違う」 ――この問題に、正面からぶつかることにしました。


📖 Before:全プラットフォームに同じ内容を投稿していた

4つのプラットフォーム、4つの読者層

自動投稿ツール自体は動いていました。CLIで1コマンド叩けば、Note・Zenn・Qiita・ブログに記事が投稿される。技術的には完成していました。

しかし、投稿された記事を各プラットフォームで実際に読んでみると、違和感だらけでした。

Noteの場合: コードブロックが延々と続く技術記事。Noteの読者はエンジニアだけではない。ストーリーや考え方を求めている人が多いのに、npm install の手順を読まされても困る。

Zennの場合: 「あなたもこんな悩みを抱えていませんか?」から始まる共感パート。Zennの読者は技術者。コードと実装方法を求めているのに、感情的なストーリーが冒頭に来ると離脱される。

Qiitaの場合: 有料化できないプラットフォームに全文を載せてしまうと、ブログへの誘導が弱くなる。

投稿先ごとに手作業で書き直す余裕はない。でも同じ内容をそのまま投稿しても読まれない。

「自動化の意味がない…」 そう感じ始めていました。

数字で見る問題

当時の記事は約5000字。そのうちコードブロックと技術的な実装説明が約2000字を占めていました。

  • Note投稿時:記事の40%が技術者以外に無意味なコード
  • Zenn投稿時:記事の30%がストーリー・感情パートで技術者に不要
  • Qiita投稿時:全文を無料公開してブログの収益機会を逃す

1つの記事を書いても、3つのプラットフォームで「中途半端」な記事になっていたのです。


転機:「消す」のではなく「見せ分ける」という発想

ある日、動画プラットフォームの配信戦略について読んでいた時、ふと気づきました。

YouTubeでは長尺、TikTokでは短尺、Instagramではビジュアル重視。同じクリエイターが、プラットフォームに合わせてコンテンツの「見せ方」を変えている。

「記事も同じことをすればいいのでは?」

1つのマスターコンテンツを書き、プラットフォームごとに「何を見せて、何を隠すか」を自動で切り替える。記事の中身は変えない。構造を変える。

この発想が、すべてを解決してくれました。


After:プラットフォーム別に最適化された4つの記事

変換ロジックを実装した結果、同じMarkdownから以下の4パターンが自動生成されるようになりました。

プラットフォーム読者層残すもの除去するもの
ブログ全読者すべてなし(完全版)
Note非エンジニアストーリー・教訓・考え方コード・技術実装・FAQ
Zenn技術者コード・実装・FAQストーリー・感情・教訓
Qiita技術者要約のみ全文(ブログに誘導)

1つのMarkdownを書くだけで、4つの「顔」を持つ記事が自動生成されます。

さらに、Zenn・Qiitaにはcanonical URLを設定し、SEO的にもブログが正規URLとして認識されるようにしました。


💭 なぜこのアプローチを選んだのか

最初に考えた方法:プラットフォームごとに記事を書く

当然、最初はそれぞれのプラットフォーム向けに個別に記事を書くことを考えました。

しかし、すぐに限界が見えました。

  • 時間コスト: 1記事4プラットフォーム分を書くと、執筆時間が4倍近くになる
  • 内容の不整合: 修正があった時、4つの記事を同期するのが地獄
  • モチベーション: 同じ内容を何度も書き直す作業は、単純に楽しくない

「もっと頭を使って、手を使わない方法はないか?」

発想の転換:記事構造を設計段階で分離する

考え方を変えました。

「記事を書く時点で、Note向けとZenn向けのセクションを意識して書く。変換は自動化する。」

つまり、記事のテンプレートそのものを再設計する。ストーリーセクションと技術セクションを明確に分離し、見出しのキーワードで自動判定して除去する仕組みを作る。

この方針に決めた理由は3つありました。

  1. 執筆は1回だけ: マスターMarkdownを1つ書けば終わり
  2. 変換は完全自動: 見出しキーワードで判定するため、手作業ゼロ
  3. メンテナンスが楽: マスターを修正すれば、全プラットフォームに反映

「正しい構造で書けば、変換は機械に任せられる」 ――これが、このアプローチの核心でした。

決め手になった3つのポイント

  1. 見出しキーワード判定の汎用性: 記事ごとにルールを設定する必要がなく、テンプレートに沿って書くだけで自動変換される
  2. 双方向の設計: NoteConverter(技術を除去)とZennConverter(ストーリーを除去)は同じアルゴリズムの裏表。一度実装すれば両方動く
  3. 段階的拡張: キーワードリストに追加するだけで、除去対象を柔軟に調整できる

🔧 具体的な実装方法

全体アーキテクチャ

┌─────────────────┐
│  Master Markdown │  ← 1つの記事ファイル
└────────┬────────┘

    ┌────┴────┐
    │ Parser  │  Frontmatter解析 + コンテンツ分離
    └────┬────┘

   ┌─────┼─────────┬──────────┐
   ▼     ▼         ▼          ▼
┌──────┐┌──────┐┌──────┐┌──────────┐
│ Note ││ Zenn ││Qiita ││   Blog   │
│Convtr││Convtr││Convtr││ Converter│
└──┬───┘└──┬───┘└──┬───┘└────┬─────┘
   │       │       │         │
   ▼       ▼       ▼         ▼
 ストーリー  技術    要約      完全版
 重視      重視   +誘導

Step 1: 見出しキーワード判定アルゴリズム

NoteConverterとZennConverterの核となるアルゴリズムは同一です。違いは「何を除去するか」のキーワードリストだけ。

class NoteConverter(PlatformConverter):
    """Note向け:技術セクションを除去してストーリー重視に変換"""

    # 技術セクションと判定するキーワード
    _TECHNICAL_KEYWORDS = [
        "具体的な実装", "実装方法", "アーキテクチャ",
        "環境構築", "セットアップ", "FAQ", "よくある質問",
        "参考リンク", "ハマりポイント", "API仕様",
    ]

    def _remove_technical_sections(self, content: str) -> str:
        lines = content.split("\n")
        result = []
        skip = False
        skip_level = 0

        for line in lines:
            heading_match = re.match(r"^(#{2,6})\s+(.+)$", line)
            if heading_match:
                level = len(heading_match.group(1))
                text = heading_match.group(2)

                if self._is_technical_heading(text):
                    skip = True        # スキップ開始
                    skip_level = level  # この見出しレベルを記録
                    continue
                elif skip and level <= skip_level:
                    skip = False       # 同レベル以上の別見出しでスキップ解除

            if not skip:
                result.append(line)

        return "\n".join(result)

ポイント: skip_level で見出しの階層を追跡することで、## 実装方法 の下の ### Step 1### Step 2 もまとめてスキップできます。次の ## まとめ のような同レベル以上の見出しが来た時点でスキップが解除されます。

Step 2: ZennConverterの逆転キーワード

ZennConverterは同じアルゴリズムを使い、キーワードリストだけが異なります。

class ZennConverter(PlatformConverter):
    """Zenn向け:ストーリーセクションを除去して技術重視に変換"""

    _STORY_KEYWORDS = [
        "悩み", "抱えていませんか",
        "ストーリー", "道のり",
        "なぜこのアプローチ", "壁にぶつかった",
        "教訓", "学んだ", "おわりに",
        "Before", "After", "転機",
        "どん底", "絶望", "突破口",
    ]

NoteConverterとZennConverterは鏡像関係です。一方が除去するセクションを、もう一方は保持する。

Step 3: NoteConverter固有の処理

Note向けには、セクション除去に加えて以下の処理も行います。

def convert(self, article: Article) -> str:
    content = self._strip_platform_blocks(article.content, "note")
    content = self._remove_technical_sections(content)  # 技術セクション除去
    content = self._remove_code_blocks(content)          # コードブロック除去
    content = self._remove_ascii_diagrams(content)       # ASCII図除去
    content = self._remove_inline_code(content)          # `バッククォート` → テキスト化
    content = self._convert_callouts(content)            # Callout → 太字テキスト
    content = self._clean_empty_lines(content)           # 連続空行を整理
    content = self._add_blog_link(content, article.slug) # ブログ誘導リンク追加
    return content

インラインコードの処理が地味に重要です。 Noteの読者は npm install という表記に馴染みがない人も多い。バッククォートを除去してプレーンテキストにすることで、より自然に読めるようになります。

Step 4: canonical URLによるSEO対策

ZennとQiitaにcanonical URLを設定し、Googleにブログが正規URLであることを伝えます。

# Zenn: frontmatterに canonical フィールドを追加
def _generate_frontmatter(self, article: Article) -> str:
    canonical_url = f"{self.BLOG_BASE_URL}/{article.slug}"
    return f"""---
title: "{article.title}"
emoji: "{article.platforms.zenn.emoji}"
topics: [{topics_str}]
published: {published}
canonical: "{canonical_url}"
---"""

# Qiita: APIペイロードに canonical_url を追加
def _build_payload(self, article, content):
    canonical_url = f"{self.BLOG_BASE_URL}/{article.slug}"
    return {
        "title": article.title,
        "body": content,
        "canonical_url": canonical_url,  # ブログを正規URLに指定
    }

これにより、同じ内容が複数サイトに存在しても、SEO的にはブログに評価が集約されます。

Step 5: 記事テンプレートの再設計

自動変換が正しく機能するには、記事の構造を設計段階で考慮する必要があります。

テンプレートを以下の11セクション構成に再設計しました。

1. フック             → 全プラットフォームに残る
2. 導入               → 全プラットフォームに残る
3. 共感パート(Before)→ Note向け(Zennでは除去)
4. ストーリー(転機)  → Note向け(Zennでは除去)
5. 成果(After)       → Note向け(Zennでは除去)
6. なぜこのアプローチか → Note向け(Zennでは除去)
7. 具体的な実装方法    → Zenn向け(Noteでは除去)
8. 壁と乗り越え方     → Note向け(Zennでは除去)
9. 教訓              → Note向け(Zennでは除去)
10. まとめ            → 全プラットフォームに残る
11. おわりに          → Note向け(Zennでは除去)

ブログには全セクションが残ります。 Note向けには技術セクション(7番)が除去され、Zenn向けにはストーリーセクション(3-6, 8-9, 11番)が除去されます。


🧱 壁にぶつかった瞬間と乗り越え方

「Note版が薄くなりすぎる」問題

最初にNoteConverterを動かした時、変換結果を見て愕然としました。

技術セクションを除去したら、残ったのはタイトルと「あなたもこんな悩みを抱えていませんか?」と「まとめ」だけ。記事の7割が消えていました。

「これでは記事として成立しない…」

原因は明白でした。元の記事がコード中心で書かれており、ストーリーや考え方のセクションがほとんど存在しなかったのです。

記事テンプレートの再設計で解決

コンバーターの問題ではなく、記事の構造の問題でした。

そこで、テンプレートに3つの新セクションを追加しました。

  • 「なぜこのアプローチを選んだのか」: 技術選定の思考プロセスを言語化する
  • 「壁にぶつかった瞬間と乗り越え方」: 失敗談をドラマチックに語る
  • 「この経験から得た教訓」: 技術を超えた汎用的な学びを書く

これらのセクションはNote変換後も残るため、コードがなくても3000〜5000字の読み応えある記事になります。

この失敗から学んだこと

「自動化は出力の品質まで含めて設計しなければ意味がない」 ということを痛感しました。

変換ロジックがいくら正確でも、入力(元記事)の構造が悪ければ、出力も悪くなる。ツールの実装だけでなく、ツールが前提とする「入力の品質」も一緒に設計する必要がありました。


🎓 この経験から得た3つの教訓

教訓1:「同じコンテンツ」は存在しない

同じ情報でも、読者によって最適な伝え方は違います。エンジニアにはコードで見せる。非エンジニアには物語で伝える。

これは記事だけの話ではなく、プレゼンテーション、社内ドキュメント、顧客への提案書――あらゆる情報発信に当てはまる原則です。

「誰に向けて書いているか」を明確にすることが、コンテンツの価値を決める。

教訓2:「構造」と「変換」を分離せよ

最初は記事の中身を直接編集しようとしていました。しかし、記事の構造(テンプレート)と変換ロジック(コンバーター)を分離したことで、どちらも独立して改善できるようになりました。

ソフトウェア設計でいう「関心の分離」が、コンテンツ制作にも有効だったのです。

教訓3:自動化は「入力の品質設計」から始まる

どんなに優秀な変換ツールを作っても、入力が悪ければ出力も悪い。ゴミを入れればゴミが出る(Garbage In, Garbage Out)。

自動化を設計する時、ツールの実装だけでなく、ツールが前提とする入力フォーマットまで一緒に設計する。テンプレートとガイドラインを整備して初めて、自動化が真価を発揮します。


🚀 応用編:OGP画像の視認性向上

今回のアップデートでは、OGP画像(記事のサムネイル)も改善しました。

タイトルをカード中央に配置

以前のOGP画像はタイトルが小さく、SNSのタイムラインで目立ちませんでした。

# タイトル文字数に応じてフォントサイズを自動調整
if len(title) <= 15:
    font_size = "72px"    # 短いタイトルは大きく
elif len(title) <= 25:
    font_size = "62px"
elif len(title) <= 40:
    font_size = "52px"
elif len(title) <= 55:
    font_size = "44px"
else:
    font_size = "38px"    # 長いタイトルでも読める最小サイズ

レイアウトも変更し、タイトルをカードの中央に配置。タグと著者名は下部に小さく表示することで、タイトルの存在感を最大化しました。


❓ よくある質問(FAQ)

Q1: テンプレートに沿わない構成の記事でも変換できますか?

A: はい。見出しキーワードマッチで判定するため、テンプレート通りでなくても、見出しに「実装」や「ストーリー」が含まれていれば自動除去されます。ただし、テンプレートに沿うことで変換精度が最も高くなります。

Q2: キーワードの誤判定はありませんか?

A: 絵文字を除去してからキーワードマッチを行うため、見出しに絵文字が含まれていても正しく判定されます。また、## 実装に至った背景 のような見出しは「実装」を含みますが、_TECHNICAL_KEYWORDS に「実装に至った」は含まれていないため、より具体的なキーワード(「具体的な実装」「実装方法」等)でマッチさせることで誤判定を抑えています。

Q3: canonical URLを設定しないとどうなりますか?

A: Googleが同一コンテンツを複数URLで検出した場合、どのURLを正規とするかを自動判定します。これによりブログではなくZennやQiitaの方が上位表示される可能性があり、ブログへの流入が減少します。canonical URLの設定で、SEO評価をブログに集約できます。


📝 まとめ:今日からできるアクションプラン

この記事で解説した内容を整理します:

  1. 記事テンプレートを設計する: ストーリーセクションと技術セクションを見出しレベルで明確に分離
  2. キーワードリストを定義する: 除去対象の見出しキーワードを決める
  3. コンバーターを実装する: 見出し走査→キーワード判定→セクション単位スキップ
  4. canonical URLを設定する: Zenn(frontmatter)、Qiita(API)でブログを正規URLに指定

今日からできる具体的なアクション:

まずは自分の記事を「ストーリーパート」と「技術パート」に分けてみてください。 その境界が見出しレベルで明確になっていれば、自動変換の仕組みは驚くほど簡単に実装できます。


🙏 おわりに:伝えたかったこと

最後まで読んでいただき、ありがとうございました。

この仕組みを作って最も変わったのは、「書く時の意識」 でした。

以前は「ブログに書くか、Noteに書くか」と投稿先を選んでいました。今は「1つの記事を書けば、すべてのプラットフォームに最適な形で届く」と分かっているので、記事の内容そのものに集中できます。

技術ブロガーにとって、「書くこと」と「届けること」は別のスキルです。書くことに集中するために、届ける部分を自動化する。その一歩として、プラットフォーム別のコンテンツ最適化は非常に効果的でした。

あなたの記事が、最適な形で、最適な読者に届くことを願っています。

質問や感想があれば、ぜひコメントやSNSでお知らせください。


📚 参考リンク


この記事が役に立ったら、ぜひシェアをお願いします!

あなたのシェアが、同じ悩みを持つ誰かの助けになります。

AD — Secure Auto Lab
この自動化、あなたの業務にも

業務自動化・AI統合を依頼しませんか?

SNS運用、データ収集、レポート生成、コンテンツ制作——手作業で回している業務をAIと自動化で解決します。ローカルLLM環境の構築にも対応。

LLM / RAG 画像生成AI SNS自動運用 データ収集
情報処理安全確保支援士 (RISS) プロジェクトマネージャ (PM)

この記事をシェア

著者を支援

tinou

情報処理安全確保支援士とPMの資格を使ってITコンサルタントとして働く傍ら、自宅で自動化とセキュリティを研究しているエンジニア