見出し画像

Notion×Slack×GASでオウンドメディアの継続発信を支える

おはようございます。Gaudiy広報のマーティン(@mrtn)です。

今日は、オウンドメディアの継続的な発信をサポートするために、Google Apps Scriptを使用してNotionからSlackへ通知を送るリマインダーを作成した話を紹介します。この記事の後半にはスクリプトも公開しています。


背景:Notionで情報発信の管理をしています

Gaudiyでは、"全員採用"という方針の下、各メンバーが入社エントリやテックブログを通じて情報発信する文化を育んでいます。この情報発信の管理は、私が開発したNotionの広報業務データベース「PRDB」を利用しており、企画から執筆、レビューまでの各フェーズをHRPRチームがサポートしながら進めています。

カレンダービューで先々までの発信予定を管理。リブランディング後で情報発信強化中

課題:多忙でオウンドメディアの更新が滞りがち

自律的な組織であるGaudiyでは、オウンドメディアの発信を各メンバーの主体性に委ねており、厳格な当番制などは導入していません。

その弊害として、大きなリリース前など業務が忙しい時期になってくると、更新予定日が守られないことがしばしば発生していました。スタートアップに忙しくない時期なんてないため、発信が途切れがちになってしまうという、あるある課題に直面していました。

ヒアリングをしていくと、メンバーは発信の重要性を理解しており、記事を書く意欲もあるものの、目の前の業務に集中するあまり執筆を忘れてしまうことがあることがわかりました。

また、HRPRチームも、多忙な時期には毎回リマインドを送ることが困難で、予定されていた記事が執筆されずうやむやになることもありました。

この課題に対処するため、Notionのデータベースに登録された自社の発信予定を利用してリマインダー機能を自動化することにしました。

作成したリマインダースクリプトの概要

このスクリプトは毎日午前9時に実行され、Notionの「PRDB」データベースから特定条件を満たすカード情報を取得し、指定したSlackチャンネルに通知します。通知対象となるのは以下の2種類のカードです。

①公開予定カードの通知

1週間後及び2週間後に公開予定のカードを特定し、それらの公開日、タイトル、執筆者、執筆者のSlack ID、カードのNotion上のURLといった関連情報を収集します。これらの情報を元に、執筆状況を確認するためのメッセージを、執筆者にメンションを付けてSlackチャンネルに投稿します。

②公開日過ぎたカードの通知

公開日が過ぎているが、ステータスが公開になっていないカードを特定し、公開日、タイトル、カードのNotion上のURLといった関連情報を収集します。これらの情報を元に、公開日の再設定を促すメッセージをPR担当にメンションを付けてSlackチャンネルに投稿します。

通知は「#all_pr」という全社員が参加しているPRチャンネルに送られます。実際の通知はこちらです👇

1週間先と2週間先でメッセージを変えている細やかさ

■リマインダー設計で気をつけたポイント

①執筆の余裕を持たせる通知タイミング
執筆者が十分な準備時間を持てるように、通知を余裕を持って2週間前と1週間前の2回に分けました。余裕をもたせた分、忘れないように2回送っています。

②コミュニケーションを妨げない人間らしい通知
コミュニケーションのきっかけになってほしかったため、通知が機械的に感じられないよう、言葉遣いや内容に工夫を工夫しました。また、圧をかけすぎないようにしました。そのうち名前やアイコンも可愛い感じにします。

③ストレスにさせない優しい仕組み
あくまでサポートするための仕組みなので、ストレスにならないようにしたい。公開日が過ぎた通知に関しては、PRチームだけに送り、PRチーム主導で公開日の再設定を行うように設定しました。

🌱 🌱 🌱

このスクリプトは、2023年10月のブランドリニューアルに向けて発信が増加することを見越して、2023年9月に実装しました。現在もこのシステムは稼働中です。

実装されたリマインダーがもたらした成果

前提としてリマインダーを入れることですべてが解決するわけではありません。リマインダーが発信を意識させてくれることと、コミュニケーションのきっかけになることが狙いでした。

PRチームは毎回声をかけるのが気が引けるし、執筆担当も進捗が芳しくない場合、気まずさもありPRチームに声をかけづらいものです。

リマインダーが自動でメンションすることにより、リマインドをきっかけにPRチームから「進捗はどうですか?悩んでいたら声をかけてくださいね!」といった声掛けが自然にしやすくなりました。

また、期間に余裕を持ったリマインドを送ることで「忙しくなってしまったので公開日を調整したい」といった事前調整のやりとりも生まれました。以前なら順番交換もなくそのまま公開日を過ぎてしまうこともあったため、素晴らしい成果といえます。良かったね!

全員が参加しているSlackチャンネルに投稿されるため、以前に比べてオウンドメディアへの意識が高まり、可視化されたことによって「●●さんのnoteが楽しみですね!」みたいな反応も見られるようになりました。

さらに、公開日を過ぎた場合でも、PRチームへのメンションによって、そのまま放置せず、公開日の再設定や発信中止の場合でも、きちんとコミュニケーションを取ることができるようになりました。

案件可視化にも役立つリマインダー氏

そんなわけで、この仕組みの導入により、日々の公開予定日のチェックとSlackでのメンション送信を自動化し、このような成果を達成しています。

オウンドメディアは即効性よりも長期的な効果が期待される施策です。継続が重要であり、発信を続けるための工夫は欠かせません。今後もサポートできる仕組みを構築していきたいと思います。自動化は楽しい。

🎁 🎁 🎁

Google Apps Scriptを公開します

このスクリプトは、私が公開している「PRDB」を利用している場合、比較的簡単に使えますが、Gaudiy向けにカスタマイズされている部分がありますので、その点について少し解説します。

PRDBはこちらです👇

データベースについて

情報発信を集約するためのデータベース「PRDB」と、社員データを管理する「Member」というデータベースを連携しています。MemberデータベースにはSlack IDも登録されており、これをロールアップ機能で取得し、通知のメンションに利用しています。

以下は、このスクリプトに関連するデータベースの属性を示したものです。

公開日判定以外のカードを取得する条件は、発信種別【オウンドメディア】に設定されたカードで、ステータスが【Lost】【Published】以外であるカードを対象としています。

Google Apps Script コード

// NotionからSlackへの通知スクリプト

// --- グローバル設定 ---
// Slack Webhook URL
const POST_URL = "https://hooks.slack.com/services/hogehogehogehoge";
// Slackの投稿先チャンネル
const SLACK_CHANNEL = "#hogehoge";
// NotionのデータベースID
const DATABASE_ID = "hogehogehogehoge";
// NotionのAPIトークン
const TOKEN = "secret_hogehogehogehoge";
// Notion APIへのリクエストヘッダー
const HEADERS = {
  "content-type": "application/json; charset=UTF-8",
  "Authorization": `Bearer ${TOKEN}`,
  "Notion-Version": "2021-08-16",
};

/**
 * Notionから特定の条件でカード情報を取得する
 */
function fetchDataFromNotion(weeksAhead, isOverdue = false) {
  const url = `https://api.notion.com/v1/databases/${DATABASE_ID}/query`;

  let currentDate = new Date();
  let targetDate = new Date();
  targetDate.setDate(targetDate.getDate() + (7 * weeksAhead));
  let formattedDate = targetDate.toISOString().split("T")[0];

  let baseFilter = [
    {
      "property": "発信種別",
      "select": {
        "equals": "オウンドメディア"
      }
    },
    {
      "property": "ステータス",
      "status": {
        "does_not_equal": "Lost"
      }
    },
    {
      "property": "ステータス",
      "status": {
        "does_not_equal": "Published"
      }
    }
  ];

  if (isOverdue) {
    baseFilter.unshift({
      "property": "公開日",
      "date": {
        "before": currentDate.toISOString().split("T")[0]
      }
    });
  } else {
    baseFilter.unshift({
      "property": "公開日",
      "date": {
        "equals": formattedDate
      }
    });
  }

  const filterData = {
    "filter": {
      "and": baseFilter
    }
  };

  const options = {
    method: "post",
    headers: HEADERS,
    payload: JSON.stringify(filterData)
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response);
  } catch (error) {
    Logger.log("Error fetching data from Notion:", error.toString());
  }
}

/**
 * Slackへメッセージを投稿する
 */
function postToSlack(title, url, slackID, publishDate, weeksAhead = null) {
  const messages = {
    1: `<@${slackID}> さん「<${url}|${title}>」は${publishDate}に公開予定です。執筆は順調ですか?`,
    2: `<@${slackID}> さん「<${url}|${title}>」は${publishDate}に公開予定です。そろそろ執筆をはじめましょう!`,
    overdue: `<!subteam^S03M1QBNQ3C> 「<${url}|${title}>」は${publishDate}に公開予定でした。公開日の再設定をお願いします。`
  };

  const messageContent = weeksAhead ? messages[weeksAhead] : messages.overdue;

  const jsonData = {
    "channel": SLACK_CHANNEL,
    "text": messageContent
  };

  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(jsonData)
  };

  try {
    UrlFetchApp.fetch(POST_URL, options);
    Logger.log(`Slack post successful for ${title} - Slack ID: ${slackID}`);
  } catch (error) {
    Logger.log("Error posting to Slack:", error.toString());
  }
}

/**
 * 指定された週数後の公開予定のカード情報を取得してSlackに通知するメイン関数
 */
function main() {
  [1, 2].forEach(weeksAhead => {
    const data = fetchDataFromNotion(weeksAhead);
    if (data && data.results.length) {
      data.results.forEach(card => {
        const title = card.properties["タイトル"].title[0].plain_text;
        const url = card.url;
        const slackID = card.properties["Slack ID"].rollup.array[0].rich_text[0].text.content;
        const publishDateRaw = new Date(card.properties["公開日"].date.start);
        const publishDate = `${publishDateRaw.getMonth() + 1}${publishDateRaw.getDate()}日`;

        postToSlack(title, url, slackID, publishDate, weeksAhead);
      });
    }
  });
    checkOverdueCards();
}

/**
 * 公開日が過ぎたカードのステータスチェックを行い、条件に該当するカードをSlackに通知する関数
 */
function checkOverdueCards() {
  const data = fetchDataFromNotion(null, true);
  if (data && data.results.length) {
    data.results.forEach(card => {
      const title = card.properties["タイトル"].title[0].plain_text;
      const url = card.url;
      const slackID = card.properties["Slack ID"].rollup.array[0].rich_text[0].text.content;
      const publishDateRaw = new Date(card.properties["公開日"].date.start);
      const publishDate = `${publishDateRaw.getMonth() + 1}${publishDateRaw.getDate()}日`;

      postToSlack(title, url, slackID, publishDate);
    });
  }
}

余談:このスクリプトはChatGPTによって作成されました

実は、私は一行も書いていません。全てはChatGPTによるものです。最近では、このスクリプトに限らず様々なスクリプトをChatGPTを利用して作成しています。近いうちに、そのプロセスについてもnoteで書きたいと思います。noteにいいねしてもらえるとMPが貯まるのでよろしくお願いします 笑

🤖 🤖 🤖

まとめ

今日は、オウンドメディアの継続発信をサポートするためにGoogle Apps Scriptでリマインダーの仕組みを作成したことを紹介しました。

日々の業務がある中でオウンドメディアを継続させるのって本当に大変ですよね!この施策だけでなく、企画面も含めてつねに様々な施策を考えています。

同じくオウンドメディアを担当されている皆さんがどんな工夫としているのか、どんな課題をお持ちなのかとても興味があります。よかったらXやnoteなどで教えてもらえると嬉しいですし、何か力になれることもあるかもです。

カジュアルに話しましょう!

例えば、PRの話題やオウンドメディア運営について、NotionやChatGPTを活用した業務効率化について、または「シンプルにGaudiyのこと気になる!」など、気軽にお申し込みください。オンラインでカジュアルに話しましょう!


この記事が参加している募集

広報の仕事

この記事が気に入ったらサポートをしてみませんか?