API自動化 Python

ツイート・YouTube・note記事の埋め込みをAPIで自動化する

埋め込み

URLを貼るだけで埋め込まれるのは、裏で型を判定しているから。その判定を、自分で呼べばいい。

外部サービスとnote記事、2種類の埋め込みを扱います

📋 この記事でわかること

  • 埋め込みが2種類に分かれている理由
  • URLからサービス種別を判定するAPI
  • embeddableを取得して本文に入れる手順
  • note記事のネイティブ埋め込みの実装
  • 対応サービスの一覧と、非対応時の代替
  • 埋め込みが崩れるケースと確認方法

noteの編集画面でURLを貼り付けると、YouTubeなら動画プレイヤーが、XならツイートのカードがそのままA表示されます。これは裏側でURLの種別を判定し、対応する埋め込みコードを取得しているからです。この処理はAPIから呼び出せるため、自動生成する記事にも埋め込みを含められます。この記事では、その実装を解説します。埋め込み機能そのものの使い方や対応サービスについてはnoteの埋め込みの記事を参照してください。

使うエンドポイント

  1. GET /api/v2/embed_by_external_api/check_type——URLの種別を判定
  2. GET /api/v2/embed_by_external_api——埋め込み情報を取得
  3. POST /api/v1/embed——note記事のネイティブ埋め込み
  4. 認証が必要です。Cookie認証を済ませてください

埋め込みは2系統ある

noteの埋め込みは、対象によって処理が分かれます。この違いを最初に理解しておくと混乱しません。

対象 使うAPI 特徴
YouTube・X・Spotifyなど外部サービス embed_by_external_api 種別判定 → 埋め込み情報取得の2段階
note内の記事 /api/v1/embed note専用のカード表示になる

外部サービスの埋め込みは、URLがどのサービスのものかを判定してから、そのサービス向けの埋め込みコードを取得します。note記事の埋め込みは、note内部の仕組みなので専用のエンドポイントを使います。

URLの種別を判定する

まず、渡されたURLがどのサービスのものかを判定します。

01_check_type.py
"""
URLがどのサービスの埋め込みに対応しているかを判定する。
"""
import json

from note_auth_client import NoteAuthClient, CookieExpiredError

SAMPLE_URLS = [
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "https://x.com/note_PR/status/1234567890",
    "https://open.spotify.com/track/xxxxxxxx",
    "https://github.com/python/cpython",
    "https://example.com/plain-page",
]


def check_type(client: NoteAuthClient, url: str) -> dict:
    """URLの埋め込み種別を判定する"""
    data = client.get_json(
        "/api/v2/embed_by_external_api/check_type",
        params={"url": url},
    )
    return data or {}


if __name__ == "__main__":
    try:
        client = NoteAuthClient()
        client.verify()

        for url in SAMPLE_URLS:
            result = check_type(client, url)
            service = result.get("type") or result.get("service") or "不明"
            print(f"{service:<16} {url[:52]}")

            # 初回は生のレスポンスも確認しておく
            if url == SAMPLE_URLS[0]:
                print("  --- 生のレスポンス ---")
                print("  " + json.dumps(result, ensure_ascii=False)[:300])

    except CookieExpiredError as e:
        print("認証エラー:", e)

返ってくる種別名は、サービスごとの識別子です。この値を次のリクエストで service パラメータとして渡します。判定できないURLの場合は、通常のリンクとして扱うことになります。

埋め込み情報を取得する

種別が分かったら、実際の埋め込み情報を取得します。

embed_client.py
"""
外部サービスの埋め込みを扱うクライアント。
"""
import logging

from note_auth_client import NoteAuthClient

logger = logging.getLogger(__name__)


class EmbedClient:

    def __init__(self, client: NoteAuthClient):
        self.client = client

    def check_type(self, url: str) -> str:
        """URLの種別を返す。判定できなければ空文字"""
        data = self.client.get_json(
            "/api/v2/embed_by_external_api/check_type",
            params={"url": url},
        )
        if not data:
            return ""
        return data.get("type") or data.get("service") or ""

    def fetch_embed(self, url: str, note_key: str,
                    service: str = None) -> dict:
        """
        埋め込み情報を取得する。
        note_key は埋め込み先の記事キー。
        """
        service = service or self.check_type(url)

        if not service:
            logger.info("埋め込みに対応していないURLです: %s", url[:60])
            return {}

        data = self.client.get_json(
            "/api/v2/embed_by_external_api",
            params={
                "url": url,
                "service": service,
                "embeddable_key": note_key,
            },
        )

        if not data:
            logger.warning("埋め込み情報を取得できません: %s", url[:60])
            return {}

        return data

    def embed_note(self, note_url: str, target_key: str,
                   height: int = 380) -> dict:
        """
        note記事をネイティブ埋め込みする。
        note_url は埋め込みたい記事のURL。
        target_key は埋め込み先の記事キー。
        """
        payload = {
            "url": note_url,
            "height": height,
            "embeddable_type": "Note",
            "embeddable_key": target_key,
        }

        res = self.client.request("POST", "/api/v1/embed", json=payload)

        if res.status_code not in (200, 201):
            logger.error("note埋め込み失敗 HTTP %s: %s",
                         res.status_code, res.text[:200])
            return {}

        data = res.json().get("data", {})

        embedded = data.get("embedded_content", {})
        return {
            "key": embedded.get("key", ""),
            "html": data.get("html_for_embed", ""),
            "raw": data,
        }


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)

    client = NoteAuthClient()
    client.verify()

    embed = EmbedClient(client)

    # 埋め込み先の記事キーを指定する
    target = "nXXXXXXXXXXXX"

    info = embed.fetch_embed(
        "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        note_key=target,
    )
    print("取得結果:", list(info.keys()) if info else "なし")

embeddable_keyが必要な理由

埋め込みは記事に紐づく形で管理されるため、「どの記事に埋め込むか」を指定する必要があります。そのため、先に下書きを作成して記事キーを取得してから、埋め込み処理を行う順序になります。記事が存在しない状態で埋め込み情報だけ取得することはできません。

note記事の埋め込み

自分の過去記事や、参考にした他人の記事をカード形式で埋め込む処理です。関連記事への導線として有効です。

02_embed_note.py
"""
note記事を別の記事に埋め込む。
自分の過去記事へ誘導する導線として使える。
"""
import time

from note_auth_client import NoteAuthClient, CookieExpiredError
from embed_client import EmbedClient


def embed_related_notes(client: NoteAuthClient, target_key: str,
                        related_urls: list, height: int = 380) -> list:
    """複数のnote記事を埋め込み、埋め込みキーの一覧を返す"""
    embed = EmbedClient(client)
    results = []

    for url in related_urls:
        print(f"埋め込み中: {url[:60]}")

        result = embed.embed_note(url, target_key, height=height)

        if result.get("key"):
            print(f"  成功: key={result['key']}")
            results.append(result)
        else:
            print("  失敗しました")

        time.sleep(2.0)

    return results


def build_embed_html(embed_key: str) -> str:
    """埋め込みキーから、本文に入れるHTMLを組み立てる"""
    # 実際に返される html_for_embed を使うのが確実だが、
    # 形式が分かっていればこの形でも動く
    return f'<p><embed data-key="{embed_key}"></p>'


if __name__ == "__main__":
    try:
        client = NoteAuthClient()
        client.verify()

        target = "nXXXXXXXXXXXX"     # 埋め込み先の記事
        related = [
            "https://note.com/someone/n/nAAAAAAAAAAA",
            "https://note.com/someone/n/nBBBBBBBBBBB",
        ]

        results = embed_related_notes(client, target, related)

        print(f"\n{len(results)} 件を埋め込みました")
        for r in results:
            if r.get("html"):
                print("  HTML:", r["html"][:80])

    except CookieExpiredError as e:
        print("認証エラー:", e)

返ってくる html_for_embed をそのまま本文に入れるのが確実です。形式を推測して自分で組み立てるより、APIが返した値を使うほうが仕様変更に強くなります

▶ 本文に画像を含める処理は、別のAPIを使います。
2段階アップロードの実装を解説しています。

画像アップロードを見る

Markdown内のURLを埋め込みに変換する

原稿に書いたURLを、自動で埋め込みに変換する処理を作ります。単独行のURLだけを対象にすると、文中のリンクと区別できます。

auto_embed.py
"""
原稿内の単独行URLを、埋め込みに変換する。

    本文の途中に書いたリンクは変換しない。
    行全体がURLだけの場合のみ埋め込み扱いにする。
"""
import re
import time

from note_auth_client import NoteAuthClient
from embed_client import EmbedClient

# 行全体がURLの場合のみマッチする
STANDALONE_URL = re.compile(r"^\s*(https?://\S+)\s*$", re.MULTILINE)

NOTE_URL = re.compile(r"^https?://note\.com/[^/]+/n/n\w+")


def convert_embeds(client: NoteAuthClient, md_text: str,
                   target_key: str, sleep_sec: float = 2.0) -> tuple:
    """
    原稿内の単独行URLを埋め込みHTMLに置き換える。
    戻り値: (置換後のテキスト, 埋め込んだ数)
    """
    embed = EmbedClient(client)
    count = 0

    def replace(match):
        nonlocal count

        url = match.group(1)

        # note記事はネイティブ埋め込みを使う
        if NOTE_URL.match(url):
            result = embed.embed_note(url, target_key)
            time.sleep(sleep_sec)

            if result.get("html"):
                count += 1
                return "\n" + result["html"] + "\n"

            print(f"  note埋め込みに失敗: {url[:60]}")
            return match.group(0)

        # 外部サービス
        info = embed.fetch_embed(url, target_key)
        time.sleep(sleep_sec)

        html = info.get("html") or info.get("html_for_embed") or ""
        if html:
            count += 1
            return "\n" + html + "\n"

        print(f"  埋め込みに対応していません: {url[:60]}")
        return match.group(0)

    result = STANDALONE_URL.sub(replace, md_text)
    return result, count


def list_urls(md_text: str) -> list:
    """原稿内の単独行URLを列挙する(事前確認用)"""
    return STANDALONE_URL.findall(md_text)


if __name__ == "__main__":
    sample = """## 参考動画

https://www.youtube.com/watch?v=dQw4w9WgXcQ

本文の中に書いた https://example.com は変換されません。

## 関連記事

https://note.com/someone/n/nAAAAAAAAAAA
"""

    urls = list_urls(sample)
    print("変換対象のURL:")
    for u in urls:
        print("  -", u)

単独行だけを対象にする理由

文中に書いたURLまで埋め込みに変換すると、文章の流れが分断されます。「詳しくはこちら(URL)」のような書き方は、リンクのままのほうが自然です。行全体がURLの場合だけを埋め込み扱いにする——このルールはMarkdownを扱う多くのツールで採用されている慣習でもあります。

対応サービスと代替手段

noteが埋め込みに対応しているサービスは限られています。非対応の場合の代替も含めて整理します。

サービス 埋め込み 表示
YouTube 対応 動画プレイヤー
X(旧Twitter) 対応 ポストのカード
Spotify 対応 再生プレイヤー
SoundCloud 対応 再生プレイヤー
Instagram 対応 投稿カード
note記事 対応(専用API) 記事カード
コード共有サービス 対応 コード表示
Amazon 条件付き 商品ウィジェット
一般のWebページ リンクカード タイトルとサムネイル
自作のiframe 非対応

技術記事でコードを載せたい場合、noteには pre タグがないため、コード共有サービスの埋め込みを使うのが現実的な選択肢になります。この点はMarkdown変換の記事でも触れています。

▼ RECOMMENDATION ▼

表現の自由度は、プラットフォームで大きく違う

noteは埋め込みできるサービスが限定されており、自作のHTMLは使えません。アメブロであれば、HTMLをある程度自由に記述でき、ASPアフィリエイトのリンクやバナーも設置できます。さらにアメプレスProを使えば、いいね・フォロー・アクセス獲得まで自動化できます。表現の自由度と収益化の両方を求めるなら、比較する価値があります。月額2,980円、365日LINEサポート付きです。

✦ ASPアフィリOK ✦ 自動いいね・フォロー ✦ 複数ブログ対応 ✦ 365日LINEサポート
アメプレスPro 公式ページを確認する →

※ 当サイトはアフィリエイト広告を含みます。

埋め込みの状態を確認する

記事を公開したあと、埋め込みが正しく表示されているかを確認する処理です。

check_embeds.py
"""
自分の記事に含まれる埋め込みを一覧し、リンク切れを検出する。
"""
import re
import time

import requests

from note_auth_client import NoteAuthClient, CookieExpiredError

URLNAME = "your_note_id"
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
      "AppleWebKit/537.36 (KHTML, like Gecko) "
      "Chrome/120.0.0.0 Safari/537.36")

EMBED_PATTERN = re.compile(r"<(embed|iframe)[^>]*>", re.IGNORECASE)
URL_IN_TAG = re.compile(r'(?:src|href|data-url)="([^"]+)"')


def collect_embeds(client: NoteAuthClient, urlname: str,
                   max_pages: int = 20) -> list:
    """全記事から埋め込みを収集する"""
    found = []

    for page in range(1, max_pages + 1):
        data = client.get_json(
            f"/api/v2/creators/{urlname}/contents",
            params={"kind": "note", "page": page},
        )
        contents = data.get("contents", [])
        if not contents:
            break

        for item in contents:
            key = item.get("key")
            if not key:
                continue

            detail = client.get_json(f"/api/v3/notes/{key}")
            body = detail.get("body") or ""

            tags = EMBED_PATTERN.findall(body)
            urls = URL_IN_TAG.findall(body)

            if tags or urls:
                found.append({
                    "key": key,
                    "title": item.get("name", ""),
                    "embed_count": len(tags),
                    "urls": [u for u in urls
                             if u.startswith("http")],
                })

            time.sleep(1.2)

        if data.get("isLastPage") or data.get("is_last_page"):
            break

    return found


def check_urls(items: list) -> list:
    """埋め込み先のURLが生きているか確認する"""
    broken = []
    session = requests.Session()
    session.headers.update({"User-Agent": UA})

    for item in items:
        for url in item["urls"]:
            try:
                res = session.head(url, timeout=10,
                                   allow_redirects=True)
                if res.status_code >= 400:
                    broken.append((item["title"], url, res.status_code))
            except requests.RequestException:
                broken.append((item["title"], url, "接続エラー"))

            time.sleep(0.8)

    return broken


if __name__ == "__main__":
    try:
        client = NoteAuthClient()
        client.verify()

        items = collect_embeds(client, URLNAME)

        print(f"=== 埋め込みを含む記事: {len(items)} 件 ===")
        for item in items:
            print(f"  {item['embed_count']} 個  {item['title'][:44]}")

        print("\n=== リンク切れの確認 ===")
        broken = check_urls(items)

        if broken:
            for title, url, status in broken:
                print(f"  [{status}] {title[:30]}")
                print(f"        {url[:70]}")
        else:
            print("  問題は見つかりませんでした")

    except CookieExpiredError as e:
        print("認証エラー:", e)

埋め込み元の動画やポストが削除されると、記事内の埋め込みは空白になります。定期的にチェックすると、古い記事の品質を保てます。特に検索から読まれ続けている記事では、この確認が効きます。

埋め込みが崩れるケース

症状 原因 対処
埋め込みが空白になる 元のコンテンツが削除された リンクに置き換える
URLを貼っても反応しない 非対応のサービス 通常のリンクにする
check_typeが空を返す 判定できないURL リンクカードとして扱う
短縮URLが埋め込まれない リダイレクト先を判定できない 展開後のURLを使う
非公開の投稿が表示されない 元の投稿の公開設定 公開されているものを使う
スマホで崩れる 埋め込みの高さ指定 heightを調整する

埋め込みは他人のコンテンツに依存する

埋め込んだ動画やポストは、投稿者の判断でいつでも削除されます。埋め込みが記事の中核を担っていると、それが消えた時点で記事が成立しなくなります。埋め込みは補助的な要素として扱い、本文だけでも意味が通る構成にしておくのが安全です。

埋め込み可否を事前に判定するツール

原稿を書く段階で、貼ろうとしているURLが埋め込みに対応しているかを確認できると手戻りが減ります。短縮URLの展開も含めた事前チェックツールです。

precheck_urls.py
"""
原稿内のURLが埋め込みに対応しているかを事前に確認する。

    python precheck_urls.py article.md

- 短縮URLを展開してから判定する
- リンク切れも同時に検出する
"""
import re
import sys
import time

import requests

from note_auth_client import NoteAuthClient, CookieExpiredError
from embed_client import EmbedClient

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
      "AppleWebKit/537.36 (KHTML, like Gecko) "
      "Chrome/120.0.0.0 Safari/537.36")

URL_PATTERN = re.compile(r"https?://[^\s)\]>\"']+")

# 展開が必要な短縮URLのホスト
SHORTENERS = {
    "bit.ly", "t.co", "goo.gl", "ow.ly", "buff.ly",
    "amzn.to", "youtu.be", "is.gd", "tinyurl.com",
}


def extract_urls(text: str) -> list:
    """重複を除いてURLを抽出する"""
    seen = set()
    result = []

    for url in URL_PATTERN.findall(text):
        url = url.rstrip(".,、。")
        if url not in seen:
            seen.add(url)
            result.append(url)

    return result


def expand(url: str, session: requests.Session) -> tuple:
    """
    短縮URLを展開し、生存確認もする。
    戻り値: (展開後のURL, ステータス)
    """
    host = url.split("/")[2] if "://" in url else ""

    try:
        res = session.head(url, timeout=12, allow_redirects=True)
        final = res.url
        status = res.status_code
    except requests.RequestException as e:
        return url, f"接続エラー({type(e).__name__})"

    if host in SHORTENERS and final != url:
        return final, status

    return final, status


def main(path: str):
    with open(path, encoding="utf-8") as f:
        text = f.read()

    urls = extract_urls(text)

    if not urls:
        print("URLが見つかりませんでした")
        return

    print(f"=== {len(urls)} 件のURLを確認します ===\n")

    try:
        client = NoteAuthClient()
        client.verify()
        embed = EmbedClient(client)
    except CookieExpiredError as e:
        print("認証エラー:", e)
        return

    session = requests.Session()
    session.headers.update({"User-Agent": UA})

    embeddable = []
    plain = []
    broken = []

    for url in urls:
        final, status = expand(url, session)

        if isinstance(status, str) or status >= 400:
            broken.append((url, status))
            print(f"[NG   ] {url[:58]}")
            print(f"         {status}")
            time.sleep(0.8)
            continue

        service = embed.check_type(final)

        if service:
            embeddable.append((final, service))
            print(f"[埋込可] {service:<14} {final[:48]}")
        else:
            plain.append(final)
            print(f"[リンク] {'':<14} {final[:48]}")

        if final != url:
            print(f"         ← 展開元 {url[:48]}")

        time.sleep(1.5)

    print("\n=== 集計 ===")
    print(f"  埋め込み可能 : {len(embeddable)} 件")
    print(f"  通常リンク   : {len(plain)} 件")
    print(f"  問題あり     : {len(broken)} 件")

    if broken:
        print("\n  修正が必要なURL:")
        for url, status in broken:
            print(f"    [{status}] {url[:60]}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("使い方: python precheck_urls.py article.md")
        sys.exit(1)

    main(sys.argv[1])

短縮URLを展開してから判定しているのが要点です。展開しないと種別が判定できず、埋め込めるはずのURLを見逃します。あわせてリンク切れも検出できるので、公開前のチェックとして一度通す価値があります。

まとめ:判定してから取得する

埋め込みの自動化は、種別を判定してから埋め込み情報を取得するという2段階です。note記事だけは専用のエンドポイントを使う点に注意すれば、実装は難しくありません。

この記事の要点

  1. 外部サービスは check_type → embed_by_external_api の2段階
  2. note記事は POST /api/v1/embed を使う
  3. 埋め込み先の記事キー(embeddable_key)が必要
  4. 先に下書きを作成してから埋め込む順序になる
  5. 返ってきた html_for_embed をそのまま使う
  6. 単独行のURLだけを埋め込み対象にする
  7. 自作のiframeは使えない。対応サービスに限られる
  8. 元コンテンツが消えると空白になる。定期確認が有効

埋め込みまで自動化できると、記事の生成が完全にスクリプトで完結します。ただし、埋め込みは他人のコンテンツに依存する要素でもあるため、使いどころは選んでください。

FAQ|noteの埋め込みAPIについてよくある質問

Q. 自作のHTMLやiframeを埋め込めますか?

できません。noteは対応サービスのURLからのみ埋め込みを生成する仕組みで、任意のHTMLを記述する手段は用意されていません。これはセキュリティ上の理由によるものです。表現の自由度を求める場合は、WordPressや自前のサイトを使うことになります。noteはあくまで「書くことに集中する」設計思想のプラットフォームです。

Q. 対応サービスの一覧はどこで確認できますか?

公式のヘルプに記載がありますが、随時更新されます。プログラムから確認したい場合は、check_type に判定させるのが確実です。返ってきた種別が空であれば非対応、値が返れば対応しているという判定になります。この記事の EmbedClient.check_type() がその処理にあたります。

Q. 埋め込みの高さを調整できますか?

note記事の埋め込みでは height パラメータで指定できます。外部サービスの埋め込みは、サービス側の仕様に依存するため調整の余地は限られます。スマートフォンでの見え方は実機で確認するのが確実です。高さが不自然な場合は、埋め込みを諦めてリンクにするという判断もあります。

Q. 埋め込みは記事の評価に影響しますか?

直接的な影響は公開されていません。ただし、埋め込みばかりで本文が少ない記事は、内容が薄いと判断される可能性があります。noteは一次情報や体験の記録を評価する方針を示しているため、他人のコンテンツを並べただけの記事は評価されにくいと考えられます。埋め込みは補助であって、記事の中身ではありません。

Q. 他人の記事を埋め込んでも問題ありませんか?

note公式が提供している機能なので、機能として使うこと自体に問題はありません。埋め込みは元記事へのリンクとして機能し、元の書き手にもアクセスが流れます。ただし、批判的な文脈で他人の記事を埋め込む場合は、相手の受け取り方に配慮してください。技術的に可能なことと、しても構わないことは別です。

Q. 埋め込んだ動画が見られなくなりました

元の動画が削除されたか、非公開に変更された可能性が高いです。この場合、記事内の埋め込みは空白になります。定期的にリンク切れをチェックし、切れているものはテキストでの説明に置き換えるのが対処法です。この記事の check_embeds.py がその確認を自動化するものです。

Q. 短縮URLが埋め込まれません

短縮URLは、リダイレクト先を解決しないと種別が判定できません。事前に展開してから渡してください。Pythonであれば requests.head(url, allow_redirects=True) で最終的なURLが取得できます。この一手間を入れておくと、埋め込みの成功率が上がります。

Q. 埋め込みを後から削除できますか?

本文から該当のタグを削除すれば、記事上には表示されなくなります。ただし、記事の更新は全フィールドを送るPUTで行うため、本文を編集する際は他のフィールドを消さないよう注意してください。マージ処理を通すことが必須です。この点は公開APIの記事で詳しく解説しています。

Q. コードを載せたいのですが良い方法はありますか?

noteには pre タグがないため、長いコードをきれいに表示する方法は限られます。実務的には、コード共有サービスに置いて埋め込むのが最も読みやすくなります。短いコードであれば code タグを含む段落として表示できますが、シンタックスハイライトは効きません。技術記事を継続的に書くなら、外部サービスとの併用を前提にした構成を考えてください。

Q. 埋め込み処理でエラーが出ます

まず embeddable_key に有効な記事キーを渡しているか確認してください。記事が存在しない状態では埋め込みを作成できません。次に、URLが対応サービスのものかを check_type で確認します。それでも失敗する場合は、レスポンスの内容を出力して、エラーメッセージを直接確認してください。非公式APIのため、仕様が変わっている可能性もあります。

アメプレスラボ編集部

AMEPRESS LAB EDITORIAL TEAM

アメブロ・note・WordPressを実際に運営し、それぞれの収益構造を数字で検証しているチーム。プラットフォームごとの得意・不得意を踏まえた、宣伝色に偏らない実践情報の発信を続けています。