API自動化 Python

noteのコメントをPythonで取得・投稿する|AST形式の書き方

コメント処理

コメントは文字列では返ってこない。木構造のJSONとして返る。

ASTの読み方さえ分かれば、あとは単純な処理です

📋 この記事でわかること

  • コメントがAST形式で返る仕組みと読み方
  • テキストからASTを組み立てる処理
  • 必須ヘッダー X-Note-Client-Code の役割
  • 返信スレッドを parent_key で扱う方法
  • 未返信のコメントを抽出するスクリプト
  • 自動返信がスパム扱いになるライン

記事にコメントが付くのは、反応の中でもっとも確かな指標です。スキは読まずに押されることもありますが、コメントは最後まで読まれた証拠になります。ただ、記事数が増えるとどこにコメントが付いているか、返信し忘れがないかを把握するのが難しくなります。APIを使えば、全記事のコメントを横断して確認できます。この記事では、その実装と、コメント特有のAST形式について解説します。

先に断っておくこと

  1. この記事では技術的な実装を解説しますが、機械的な自動返信は推奨しません
  2. コメントは人と人のやり取りであり、定型文の返信は関係を損ないます
  3. 自動化して価値があるのは「取得と可視化」までです
  4. 投稿系は、返信忘れを防ぐための補助として使ってください

コメントAPIの構成

使うのは /api/v3/notes/{key}/note_comments です。v1系のエンドポイントは空配列を返すよう変更されているため、v3を使う必要があります。

メソッド パス 内容
GET /api/v3/notes/{key}/note_comments コメント一覧
POST /api/v3/notes/{key}/note_comments コメント投稿
PUT /api/v3/notes/{key}/note_comments/{comment_key} コメント編集
DELETE /api/v3/notes/{key}/note_comments/{comment_key} コメント削除

取得時のパラメータは次のとおりです。

パラメータ 意味
page 数値 ページ番号
per_page 数値 1ページの件数
order newest / oldest 並び順
parent_key nc形式のキー 指定すると返信スレッドを取得

AST形式とは何か

コメントの本文は、単なる文字列ではなく木構造のJSONとして返ります。ASTは抽象構文木の略で、文書の構造をプログラムが扱いやすい形で表現したものです。

ASTの構造(1段落のコメント)
{
  "type": "root",
  "children": [
    {
      "type": "element",
      "tag_name": "p",
      "children": [
        {
          "type": "text",
          "value": "記事を読みました。参考になりました。"
        }
      ]
    }
  ]
}

階層は3種類のノードで構成されます。

  • 1
    root——最上位のノード。children に段落を持ちます。
  • 2
    element——タグに相当するノード。tag_namep なら段落です。
  • 3
    text——実際の文字が入るノード。value に文字列があります。

なぜ文字列ではないのか

HTMLをそのまま受け取ると、悪意のあるスクリプトを埋め込まれる危険があります。構造化された形式で受け取れば、許可されたノードだけを処理すればよく、安全性が高まります。手間は増えますが、理にかなった設計です。同様の方式は、他のサービスのリッチテキスト処理でも使われています。

ASTとテキストを相互変換する

実装で最初に必要になるのが、この変換処理です。取得したASTを読める形にし、送るテキストをASTに組み立てます。

ast_utils.py
"""
noteのコメントAST(抽象構文木)とテキストを相互変換する。
"""


def ast_to_text(node) -> str:
    """
    ASTからプレーンテキストを取り出す。
    段落ごとに改行を入れる。
    """
    if node is None:
        return ""

    # 文字列がそのまま入っている場合もある
    if isinstance(node, str):
        return node

    if not isinstance(node, dict):
        return ""

    node_type = node.get("type")

    if node_type == "text":
        return node.get("value", "")

    children = node.get("children") or []
    parts = [ast_to_text(child) for child in children]

    if node_type == "element":
        tag = node.get("tag_name", "")
        if tag == "br":
            return "\n"
        if tag in ("p", "div"):
            return "".join(parts) + "\n"

    return "".join(parts)


def text_to_ast(text: str) -> dict:
    """
    プレーンテキストからASTを組み立てる。
    改行で段落を分ける。
    """
    if not text or not text.strip():
        raise ValueError("本文が空です")

    lines = [l.strip() for l in text.split("\n")]

    children = []
    for line in lines:
        if not line:
            continue

        children.append({
            "type": "element",
            "tag_name": "p",
            "children": [
                {"type": "text", "value": line}
            ],
        })

    if not children:
        raise ValueError("有効な行がありません")

    return {"type": "root", "children": children}


def describe_ast(node, depth: int = 0):
    """ASTの構造を確認する(デバッグ用)"""
    if not isinstance(node, dict):
        print("  " * depth + repr(node)[:60])
        return

    node_type = node.get("type", "?")

    if node_type == "text":
        value = node.get("value", "")
        print("  " * depth + f"text: {value[:40]}")
        return

    tag = node.get("tag_name", "")
    label = f"{node_type}" + (f"({tag})" if tag else "")
    print("  " * depth + label)

    for child in node.get("children") or []:
        describe_ast(child, depth + 1)


if __name__ == "__main__":
    sample_text = "記事を読みました。\n参考になりました。"

    ast = text_to_ast(sample_text)
    print("=== 組み立てたAST ===")
    describe_ast(ast)

    print("\n=== 戻したテキスト ===")
    print(repr(ast_to_text(ast)))

X-Note-Client-Codeヘッダー

コメントの投稿・編集・削除には、X-Note-Client-Code というヘッダーが必要です。これが無いとリクエストが拒否されます。

client_code.py
"""
X-Note-Client-Code を扱う。

このヘッダーは、ブラウザがコメント投稿時に送っている識別子。
DevToolsのネットワークタブで、実際のコメント投稿リクエストを
確認すると値が分かる。
"""
import os

from dotenv import load_dotenv

load_dotenv()


def get_client_code() -> str:
    """
    .env から X-Note-Client-Code の値を読む。

    .env の例:
        NOTE_CLIENT_CODE=64文字の16進数
    """
    code = os.getenv("NOTE_CLIENT_CODE", "").strip()

    if not code:
        raise ValueError(
            "NOTE_CLIENT_CODE が設定されていません。\n"
            "ブラウザでコメントを投稿し、DevToolsのネットワークタブから\n"
            "リクエストヘッダーの X-Note-Client-Code を確認して\n"
            ".env に設定してください"
        )

    if len(code) != 64:
        print(f"警告: 想定と長さが違います({len(code)} 文字)")

    return code


def comment_headers() -> dict:
    """コメント投稿用のヘッダーを組み立てる"""
    return {
        "X-Note-Client-Code": get_client_code(),
        "Content-Type": "application/json",
    }


if __name__ == "__main__":
    try:
        code = get_client_code()
        print(f"取得しました(先頭8文字: {code[:8]}...)")
    except ValueError as e:
        print(e)

値の取得方法

このヘッダーの値は、ブラウザから取得する必要があります。noteの記事でコメントを1件投稿し、DevToolsのネットワークタブでそのリクエストを開いてください。リクエストヘッダーの中に X-Note-Client-Code があります。Cookieと同様に、この値も外部に出さないよう管理してください

コメントを扱うクライアント

取得・投稿・削除をまとめたクラスを作ります。

comment_client.py
"""
noteのコメント操作をまとめたクライアント。
"""
import logging

from note_auth_client import NoteAuthClient
from ast_utils import ast_to_text, text_to_ast
from client_code import get_client_code

logger = logging.getLogger(__name__)


class CommentClient:

    def __init__(self, client: NoteAuthClient,
                 client_code: str = None):
        self.client = client
        self._client_code = client_code

    def _headers(self) -> dict:
        code = self._client_code or get_client_code()
        return {"X-Note-Client-Code": code}

    # ---- 取得 ----

    def list_comments(self, note_key: str, order: str = "newest",
                      per_page: int = 20,
                      max_pages: int = 10) -> list:
        """記事のコメント一覧を取得する"""
        comments = []

        for page in range(1, max_pages + 1):
            data = self.client.get_json(
                f"/api/v3/notes/{note_key}/note_comments",
                params={
                    "page": page,
                    "per_page": per_page,
                    "order": order,
                },
            )

            items = data if isinstance(data, list) else data.get("data", [])
            if not items:
                break

            comments.extend(items)

            if len(items) < per_page:
                break

        return comments

    def list_replies(self, note_key: str, parent_key: str) -> list:
        """特定コメントへの返信を取得する"""
        data = self.client.get_json(
            f"/api/v3/notes/{note_key}/note_comments",
            params={"parent_key": parent_key, "per_page": 50},
        )
        return data if isinstance(data, list) else data.get("data", [])

    @staticmethod
    def to_readable(comment: dict) -> dict:
        """コメントを扱いやすい形に変換する"""
        user = comment.get("user") or {}

        return {
            "key": comment.get("key", ""),
            "text": ast_to_text(comment.get("comment")).strip(),
            "user_key": user.get("key", ""),
            "nickname": user.get("nickname", ""),
            "urlname": user.get("urlname", ""),
            "is_root": comment.get("is_root", True),
            "created_at": comment.get("created_at",
                                      comment.get("createdAt", "")),
        }

    # ---- 投稿 ----

    def post_comment(self, note_key: str, text: str,
                     parent_key: str = None,
                     dry_run: bool = True) -> dict:
        """
        コメントを投稿する。
        parent_key を指定すると返信になる。
        """
        payload = {
            "comment": text_to_ast(text),
            "acknowledgement": False,
        }

        if parent_key:
            payload["parent_key"] = parent_key

        print("=== 投稿内容 ===")
        print("  記事:", note_key)
        print("  種別:", "返信" if parent_key else "新規コメント")
        print("  本文:", text[:80])

        if dry_run:
            print("\n[確認モード] 実際には投稿していません")
            return {}

        res = self.client.request(
            "POST",
            f"/api/v3/notes/{note_key}/note_comments",
            json=payload,
            headers=self._headers(),
        )

        if res.status_code not in (200, 201):
            logger.error("投稿失敗 HTTP %s: %s",
                         res.status_code, res.text[:200])
            return {}

        try:
            data = res.json().get("data", {})
        except ValueError:
            data = {}

        logger.info("コメントを投稿しました")
        return data

    def edit_comment(self, note_key: str, comment_key: str,
                     text: str, dry_run: bool = True) -> bool:
        """自分のコメントを編集する"""
        payload = {"comment": text_to_ast(text)}

        print(f"=== 編集: {comment_key} ===")
        print("  新しい本文:", text[:80])

        if dry_run:
            print("\n[確認モード] 実際には送信していません")
            return False

        res = self.client.request(
            "PUT",
            f"/api/v3/notes/{note_key}/note_comments/{comment_key}",
            json=payload,
            headers=self._headers(),
        )

        if res.status_code not in (200, 201):
            logger.error("編集失敗 HTTP %s", res.status_code)
            return False

        return True

    def delete_comment(self, note_key: str,
                       comment_key: str) -> bool:
        """コメントを削除する(comment_keyはnc形式)"""
        res = self.client.request(
            "DELETE",
            f"/api/v3/notes/{note_key}/note_comments/{comment_key}",
            headers=self._headers(),
        )

        if res.status_code not in (200, 204):
            logger.error("削除失敗 HTTP %s", res.status_code)
            return False

        logger.info("コメントを削除しました: %s", comment_key)
        return True


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

    client = NoteAuthClient()
    client.verify()

    cc = CommentClient(client)

    note_key = "nXXXXXXXXXXXX"
    comments = cc.list_comments(note_key)

    print(f"=== コメント {len(comments)} 件 ===")
    for c in comments:
        r = cc.to_readable(c)
        print(f"\n  {r['nickname']}({r['created_at'][:10]})")
        print(f"  {r['text'][:100]}")

▶ スキやフォローの操作も同じように実装できますが、注意点があります。
自動化の境界線について解説しています。

スキ・フォローの記事を見る

未返信のコメントを見つける

実用上もっとも価値があるのがこの処理です。全記事を横断して、まだ返信していないコメントを洗い出します。

find_unreplied.py
"""
全記事を横断して、未返信のコメントを抽出する。
返信忘れを防ぐための確認スクリプト。
"""
import time

from note_auth_client import NoteAuthClient, CookieExpiredError
from comment_client import CommentClient

URLNAME = "your_note_id"
SLEEP_SEC = 1.5
MAX_ARTICLES = 100


def fetch_my_notes(client: NoteAuthClient, urlname: str,
                   max_pages: int = 50) -> list:
    items = []

    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

        items.extend(contents)

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

        time.sleep(1.2)

    return items


def has_comments(item: dict) -> bool:
    """コメント数のフィールドで事前に絞り込む"""
    for key in ("commentCount", "comment_count"):
        if item.get(key):
            return True
    return False


def find_unreplied(client: NoteAuthClient, my_user_key: str,
                   notes: list) -> list:
    """自分が返信していないコメントを抽出する"""
    cc = CommentClient(client)
    unreplied = []

    targets = [n for n in notes if has_comments(n)][:MAX_ARTICLES]
    print(f"コメントがある記事: {len(targets)} 本\n")

    for i, note in enumerate(targets, start=1):
        note_key = note.get("key")
        title = note.get("name", "")

        if not note_key:
            continue

        comments = cc.list_comments(note_key)
        readable = [cc.to_readable(c) for c in comments]

        # 自分以外のルートコメントを抽出
        others = [
            r for r in readable
            if r["is_root"] and r["user_key"] != my_user_key
        ]

        for parent in others:
            replies = cc.list_replies(note_key, parent["key"])
            reply_users = {
                cc.to_readable(r)["user_key"] for r in replies
            }

            if my_user_key not in reply_users:
                unreplied.append({
                    "note_key": note_key,
                    "title": title,
                    "comment": parent,
                })

            time.sleep(SLEEP_SEC)

        print(f"[{i}/{len(targets)}] {title[:40]}"
              f"(コメント {len(others)} 件)")

        time.sleep(SLEEP_SEC)

    return unreplied


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

    my_user_key = user.get("key") or str(user.get("id", ""))

    notes = fetch_my_notes(client, URLNAME)
    print(f"記事 {len(notes)} 本を確認します\n")

    unreplied = find_unreplied(client, my_user_key, notes)

    print(f"\n=== 未返信のコメント: {len(unreplied)} 件 ===")

    for item in unreplied:
        c = item["comment"]
        print(f"\n[{item['title'][:40]}]")
        print(f"  {c['nickname']}({c['created_at'][:10]})")
        print(f"  {c['text'][:120]}")
        print(f"  https://note.com/notes/{item['note_key']}")


if __name__ == "__main__":
    main()

この処理は取得だけを行い、返信は人間が書くという設計です。返信すべきコメントの一覧が出れば、あとは読んで自分の言葉で返すだけになります。これが自動化の適切な使い方です。

▼ RECOMMENDATION ▼

反応をもらう前に、読まれる状態を作る

コメントは、読まれた記事にしか付きません。返信管理を整えるより先に、読む人を増やす段階の人が大半です。アメブロであれば、アメプレスProがいいね・フォロー・アクセス獲得を自動で回し、読者との接点そのものを増やせます。noteと違ってASPアフィリエイトも使えるため、収益化まで含めた設計が可能です。月額2,980円、365日LINEサポート付きです。

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

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

コメントを集計・分析する

どの記事にコメントが集まるかを把握すると、読者の関心が見えてきます。

comment_stats.py
"""
コメントを集計して、傾向を把握する。
- どの記事にコメントが集まるか
- 誰が繰り返しコメントしてくれているか
- コメントの長さの分布
"""
import csv
import time
from collections import Counter
from datetime import datetime

from note_auth_client import NoteAuthClient, CookieExpiredError
from comment_client import CommentClient
from find_unreplied import fetch_my_notes, has_comments

URLNAME = "your_note_id"
OUTPUT = "comments.csv"


def collect_all(client: NoteAuthClient, notes: list,
                limit: int = 80) -> list:
    cc = CommentClient(client)
    rows = []

    targets = [n for n in notes if has_comments(n)][:limit]

    for i, note in enumerate(targets, start=1):
        note_key = note.get("key")
        title = note.get("name", "")

        if not note_key:
            continue

        for c in cc.list_comments(note_key):
            r = cc.to_readable(c)
            rows.append({
                "note_key": note_key,
                "title": title,
                "comment_key": r["key"],
                "nickname": r["nickname"],
                "urlname": r["urlname"],
                "text": r["text"].replace("\n", " "),
                "length": len(r["text"]),
                "created_at": r["created_at"],
            })

        print(f"[{i}/{len(targets)}] {title[:40]}")
        time.sleep(1.5)

    return rows


def analyze(rows: list):
    if not rows:
        print("コメントがありません")
        return

    print(f"\n=== コメント {len(rows)} 件 ===")

    by_article = Counter(r["title"] for r in rows)
    print("\n--- コメントが多い記事 ---")
    for title, count in by_article.most_common(10):
        print(f"  {count:>3} 件  {title[:44]}")

    by_user = Counter(
        r["nickname"] for r in rows if r["nickname"]
    )
    print("\n--- よくコメントをくれる方 ---")
    for name, count in by_user.most_common(10):
        print(f"  {count:>3} 件  {name[:24]}")

    lengths = [r["length"] for r in rows]
    avg = sum(lengths) / len(lengths)
    print(f"\n--- コメントの長さ ---")
    print(f"  平均: {avg:.0f} 文字")
    print(f"  最長: {max(lengths)} 文字")

    long_comments = [r for r in rows if r["length"] > 100]
    print(f"  100文字超: {len(long_comments)} 件")

    if long_comments:
        print("\n  --- 長文コメントの例 ---")
        for r in long_comments[:3]:
            print(f"  [{r['title'][:30]}]")
            print(f"  {r['text'][:100]}...")


def save_csv(rows: list, path: str = OUTPUT):
    if not rows:
        return

    with open(path, "w", encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
        writer.writeheader()
        writer.writerows(rows)

    print(f"\n{path} に保存しました")


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

    notes = fetch_my_notes(client, URLNAME)
    rows = collect_all(client, notes)

    analyze(rows)
    save_csv(rows)


if __name__ == "__main__":
    main()

長いコメントに注目する

100文字を超えるコメントは、読者が時間をかけて書いたものです。そこには記事のどこが響いたかという情報が詰まっています。スキ数を見るより、こうしたコメントを読み返すほうが、次に書くべきことが見えてきます。

自動返信について

技術的には、コメントを検出して自動で返信を投稿することも可能です。しかし推奨しません。理由を整理します。

やり方 結果 評価
すべてに定型文で返信 すぐに機械的だと気づかれる 関係を損なう
キーワードで返信を出し分け 的外れな返信が混ざる リスクが高い
未返信を検出して通知 返信漏れが減る 推奨
返信の下書きだけ用意する 人が確認して修正できる 条件付きで可
他人の記事に自動コメント スパム判定の対象 してはいけない

他人の記事への自動コメントは明確にNG

宣伝目的で他人の記事に機械的にコメントを投稿する行為は、スパムとして扱われます。アカウントの制限や、最悪の場合は利用停止の対象になります。また、そうした行為で得られる関心は一時的なもので、長期的には自分の評判を落とします。この記事で解説した投稿系のAPIは、自分の記事への返信に限って使ってください。関連する話題は通報の記事でも扱っています。

つまずきやすい点

症状 原因 対処
コメントが空配列で返る v1のエンドポイントを使っている v3を使う
投稿で403が返る X-Note-Client-Codeが無い ヘッダーを付ける
本文が取り出せない ASTを文字列として扱っている ast_to_textを通す
投稿すると本文が壊れる 文字列をそのまま送っている text_to_astでAST化する
削除で404 数値idを渡している nc形式のcomment_keyを使う
返信が取得できない parent_keyを指定していない 親コメントのkeyを渡す
コメント欄が無い 記事側でコメントを無効化している 記事の設定を確認する

まとめ:取得は自動化、返信は人が書く

コメントAPIの技術的な要点はAST形式と必須ヘッダーの2つです。この2つを押さえれば実装は難しくありません。ただし、使い方の判断のほうが重要です。

この記事の要点

  1. コメントは /api/v3/notes/{key}/note_comments。v1は空配列を返す
  2. 本文はAST(root / element / text の木構造)で返る
  3. 投稿・編集・削除には X-Note-Client-Code ヘッダーが必須
  4. 返信は parent_key で親コメントを指定する
  5. 削除は数値idではなく nc形式の comment_key
  6. 未返信の検出は実用的で、リスクもない
  7. 定型文の自動返信は関係を損なう
  8. 他人の記事への自動コメントはスパム扱いになる

まずは未返信コメントの抽出だけを動かしてみてください。返信忘れが可視化されるだけでも、運用の質は上がります。

FAQ|コメントAPIについてよくある質問

Q. なぜコメントはHTMLではなくASTなのですか?

安全性のためです。HTMLを直接受け取ると、スクリプトの埋め込みなど不正な内容が混入する危険があります。構造化された形式で受け取れば、サーバー側で許可されたノードだけを処理でき、危険な要素を確実に排除できます。扱いは面倒になりますが、設計としては妥当です。

Q. X-Note-Client-Codeの値はどう取得しますか?

ブラウザで実際にコメントを1件投稿し、DevToolsのネットワークタブでそのリクエストを開いてください。リクエストヘッダーの中に含まれています。64文字の16進数です。この値もCookieと同様に認証に関わる情報なので、.env で管理し、外部に出さないようにしてください。

Q. 他人の記事のコメントも取得できますか?

公開されているコメントは取得できます。ただし、大量に収集して分析するような使い方は、コメントを書いた人の想定を超えています。自分の記事のコメント管理に使うのが本来の用途です。他人の記事のコメントは、読んで参考にする程度に留めてください。

Q. コメントの通知をAPIで受け取れますか?

通知件数を返す /api/v3/notice_counts というエンドポイントはありますが、コメントに限定した通知の取得は確認されていません。定期的にコメント一覧を取得して差分を見る、という方式が現実的です。1日1回程度の確認であれば、負荷も小さく済みます。

Q. 自動返信は本当にダメですか?

技術的には可能ですが、おすすめしません。読者はコメントを書くとき、書き手からの反応を期待しています。明らかな定型文が返ってくると、期待を裏切られたと感じます。返信が追いつかないなら、返さないほうがまだ良いくらいです。自動化するなら、返信忘れを検出して通知するところまでに留めてください。

Q. コメント欄を閉じることはできますか?

記事の設定でコメントのオンオフを切り替えられます。この設定はnoteプレミアムの機能です。APIから公開処理を行う際は disable_comment フィールドで指定できますが、フルペイロード方式のため他の設定を巻き込まないよう注意してください。

Q. 不快なコメントを削除できますか?

自分の記事に付いたコメントは削除できます。この記事の delete_comment() がその処理です。悪質なものについては、削除に加えて通報も検討してください。ただし、単に批判的というだけのコメントを削除すると、かえって反発を招くこともあります。判断は慎重に行ってください。

Q. コメントに画像やリンクを含められますか?

ASTの構造上、element ノードのタグ名を変えれば理論的には可能ですが、note側で許可されているタグは限られます。この記事の実装は段落(p)のみを扱う最小構成です。リンクを含めたい場合は、URLをそのままテキストとして書けば、note側で自動的にリンクとして表示されることが多いようです。

Q. 未返信の検出に時間がかかります

記事ごとにコメント一覧を取得し、さらに返信も取得するため、リクエスト数が多くなります。この記事のコードでは、コメント数のフィールドで事前に絞り込むことで、コメントが無い記事へのリクエストを省いています。それでも記事数が多い場合は、対象を最近の記事に限定するか、実行頻度を週1回程度に落としてください。

Q. コメントが付かないのですが

コメントはスキより敷居が高い行動なので、付かないのが普通です。付きやすくするには、記事の最後に問いかけを入れる、自分の意見を明確に書いて反応の余地を作る、といった工夫があります。ただし、コメントの数を目的にすると本末転倒です。付いたときに丁寧に返すことのほうが、結果的に関係を作ります。

アメプレスラボ編集部

AMEPRESS LAB EDITORIAL TEAM

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