Notiondesk API を使用して、一般的なヘルプセンターのタスクを自動化します。

Notiondesk API の例: GitHub Actions から同期、記事をマークダウンとしてバックアップ、Slack にフィードバックを投稿、検索結果がない検索を報告。

4 分で読めます

この記事では、チームが最も自動化するタスク(CIパイプラインからのNotion編集の公開、すべての記事のMarkdown形式でのバックアップ、Slackへの否定的なフィードバックの送信、ヘルプセンターで回答できない検索の報告、同期失敗の検出、複数のヘルプセンターへの1つのブランドの適用など)に対応した、すぐに使えるNotiondesk APIスクリプトをまとめています。レシピをコピーし、2つの環境変数を設定して実行してください。

始める前に

  • 両方を環境変数として設定します。
export NOTIONDESK_API_KEY="nd_wsk_..."
export NOTIONDESK_SITE_ID="c74781f6-1aea-4e17-ab23-bbb8b4d174b6"

JavaScriptのレシピにはNode.js 18以降が必要です。

小さな共有ヘルパー

このファイルをnotiondesk.mjsという名前でスクリプトと同じ場所に保存してください。このファイルはヘッダーを送信し、APIからの応答429 rate_limitedを待ってから、リストのすべてのページを読み取ります。

GitHub Actionsからヘルプセンターを同期する

範囲: sync

GitHub ActionsワークフローからNotionの編集内容を公開します。たとえば、リリース直後に公開します。キーをリポジトリシークレットNOTIONDESK_API_KEYとして、ヘルプセンターIDをリポジトリ変数NOTIONDESK_SITE_IDとして保存します。

export const SITE_ID = process.env.NOTIONDESK_SITE_ID;

export async function api(path, options = {}) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const response = await fetch(`https://api.notiondesk.so${path}`, {
      ...options,
      headers: {
        Authorization: `Bearer ${process.env.NOTIONDESK_API_KEY}`,
        "nd-version": "2026-10-16",
        ...options.headers,
      },
    });

    if (response.status === 429) {
      const seconds = Number(response.headers.get("Retry-After")) || 5;
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      continue;
    }
    if (!response.ok) {
      throw new Error(`${response.status} ${await response.text()}`);
    }
    return response.status === 204 ? null : response.json();
  }
  throw new Error(`Still rate limited on ${path}`);
}

export async function listAll(path) {
  const items = [];
  let cursor = null;
  do {
    const url = new URL(`https://api.notiondesk.so${path}`);
    url.searchParams.set("page_size", "100");
    if (cursor) {
      url.searchParams.set("cursor", cursor);
    }
    const page = await api(`${url.pathname}${url.search}`);
    items.push(...page.data);
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
  return items;
}
name: Sync help center
on:
  workflow_dispatch:
  release:
    types: [published]

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger a Notiondesk sync
        run: |
          status=$(curl -s -o response.json -w "%{http_code}" -X POST \
            "https://api.notiondesk.so/sites/${{ vars.NOTIONDESK_SITE_ID }}/sync" \
            -H "Authorization: Bearer ${{ secrets.NOTIONDESK_API_KEY }}" \
            -H "nd-version: 2026-10-16" \
            -H "Idempotency-Key: github-${{ github.run_id }}-${{ github.run_attempt }}")
          if [ "$status" = "409" ]; then echo "A sync is already running."; exit 0; fi
          if [ "$status" != "204" ]; then cat response.json; exit 1; fi
          echo "Sync queued."
  • 204 は、同期処理がキューに登録されたことを意味します。バックグラウンドで実行されます。
  • 409 conflict_error は、ヘルプセンターの同期が既にキューに登録されているか実行中であることを意味するため、別の同期を開始することなくこのステップは成功します。
  • Idempotency-Key は、再試行されたリクエストを 2 回何もキューに入れません。
自動翻訳が有効になっている間は、同期のたびに変更されたコンテンツが翻訳され、その分の料金が発生します。ワークフローは、短いスケジュールではなく、実際の編集後にトリガーしてください。詳細については、「Notiondesk API を使用して Notion の同期をトリガーする」を参照してください。

すべての記事をマークダウン形式でバックアップする

範囲: read

ヘルプセンターで提供しているすべての言語の記事を、マークダウンファイルとして保存します。

バックアップには下書きとアーカイブされた記事が含まれます。閲覧は1分あたり120件に制限されているため、500件の記事があるヘルプセンターの場合、言語ごとに約5分かかります。制限に達すると、ヘルパーは自動的に待機します。

毎日Slackに否定的なフィードバックを送る

適用範囲: read のみ

過去24時間の否定的なフィードバックを、受信Webhook経由でSlackチャンネルに投稿します。SLACK_WEBHOOK_URLを設定し、スクリプトを1日1回実行してください。

フィードバックリストは新しい順に表示されるため、スクリプトは24時間以上前の最初のエントリでページングを停止します。

import { mkdir, writeFile } from "node:fs/promises";
import { api, listAll, SITE_ID } from "./notiondesk.mjs";

const site = await api(`/sites/${SITE_ID}`);

for (const locale of site.supported_locales) {
  await mkdir(`backup/${locale}`, { recursive: true });
  const articles = await listAll(`/sites/${SITE_ID}/articles?locale=${locale}`);

  for (const article of articles) {
    const full = await api(`/sites/${SITE_ID}/articles/${encodeURIComponent(article.id)}?locale=${locale}`);
    const frontMatter = `---\ntitle: ${JSON.stringify(full.title ?? "")}\nstatus: ${full.status}\nurl: ${full.url ?? ""}\n---\n\n`;
    await writeFile(`backup/${locale}/${encodeURIComponent(article.id)}.md`, frontMatter + full.text);
    if (full.truncated) {
      console.warn(`${locale}/${article.id} is longer than 200,000 bytes and was truncated`);
    }
  }
  console.log(`${locale}: ${articles.length} articles saved`);
}
import { api, SITE_ID } from "./notiondesk.mjs";

const since = Date.now() - 24 * 60 * 60 * 1000;
const escapeSlack = (text) => (text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

async function feedbackSince(timestamp) {
  const entries = [];
  let cursor = null;
  do {
    const query = new URLSearchParams({ page_size: "100" });
    if (cursor) {
      query.set("cursor", cursor);
    }
    const page = await api(`/sites/${SITE_ID}/feedback?${query}`);
    const recent = page.data.filter((entry) => Date.parse(entry.submitted_at) >= timestamp);
    entries.push(...recent);
    cursor = page.has_more && recent.length === page.data.length ? page.next_cursor : null;
  } while (cursor);
  return entries;
}

const negative = (await feedbackSince(since)).filter(
  (entry) => entry.reaction_group === "negative" || entry.reaction_group === "not_helpful",
);

if (negative.length > 0) {
  const lines = negative.map(
    (entry) => `• *${escapeSlack(entry.article_title)}* (${entry.article_locale}): ${escapeSlack(entry.message) || "_no message_"}`,
  );
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ text: `${negative.length} negative feedback in the last 24 hours\n${lines.join("\n")}` }),
  });
}
フィードバックは読者によって書かれます。このスクリプトは、のみを保持するキーで実行し、投稿する前にテキストをエスケープし、書き込み可能なキーを保持する LLM に渡さないでください。__ ND_TERM_5bcd8dee__ API を使用した読者フィードバックのエクスポートを参照してください。read

毎週検索結果のないレポート

範囲: read

先週、読者が記事を見つけられなかった検索キーワードを一覧表示します。それぞれの検索クエリは、新しい記事の候補、またはより適切なタイトルの候補となります。

各クエリリストには最大50行が格納されます。分析レポートでは、ヘルプセンターごとに1分あたり30件のリクエストという予算が共有されます。詳細については、「Notiondesk API を使用したヘルプセンター分析の取得」を参照してください。

最終同期に失敗した記事を探す

範囲: read、さらに再試行のための sync

まずNotionで各記事を修正し、その後POST /sites/{site_id}/articles/{article_id}/syncと再度同期してください。

すべてのヘルプセンターに1つのブランドを適用する

適用範囲: read および admin

ワークスペース内の他のすべてのヘルプセンターに、1つのヘルプセンターの色とフォントをコピーします。コピー元のヘルプセンターにSOURCE_SITE_IDを設定してください。

ブランディングの更新ごとにターゲットのヘルプセンター全体が更新されるため、スクリプトはヘルプセンターごとに1つのリクエストを送信します。#fff のような短い形式で保存された色は拒否されます。まず、ソースヘルプセンターのダッシュボードで修正してください。__ ND_TERM_5bcd8dee__ API を使用してヘルプセンターのブランディングとテーマをカスタマイズする方法を参照してください。

移行後にインポートのリダイレクトが発生する

適用範囲: read および write

別のヘルプセンターツールから移行する場合、リダイレクトインポートスクリプトは古いパスのCSVファイルを読み込み、行ごとに1つのリダイレクトを作成します。詳細については、「Notiondesk APIを使用したページリダイレクトの管理」を参照してください。

import { api, SITE_ID } from "./notiondesk.mjs";

const end = new Date();
end.setUTCHours(0, 0, 0, 0);
const start = new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000);
const toApiDate = (date) => `${date.toISOString().slice(0, 19)}Z`;

const query = new URLSearchParams({
  start_date: toApiDate(start),
  end_date: toApiDate(end),
  date_scale: "day",
});
const report = await api(`/sites/${SITE_ID}/analytics/search?${query}`);

console.log(`Searches: ${report.searches.total} (${report.searches.percentage_difference}% vs previous week)`);
console.log(`Without results: ${report.searches_without_results.total}`);
console.log(`Click-through rate: ${report.click_through_rate.current.toFixed(1)}%`);

for (const { query: text, searches } of report.searches_without_results.queries) {
  console.log(`${searches}\t${text}`);
}
import { api, listAll, SITE_ID } from "./notiondesk.mjs";

const articles = await listAll(`/sites/${SITE_ID}/articles`);
const failed = articles.filter((article) => article.sync_failed || article.sync_timeout);

for (const article of failed) {
  const reason = article.sync_timeout ? "timed out" : article.sync_error_code ?? "failed";
  console.log(`${article.title}: ${reason} ${article.notion_article_page_url ?? ""}`);
}
import { createHash } from "node:crypto";
import { api, listAll } from "./notiondesk.mjs";

const sourceId = process.env.SOURCE_SITE_ID;
const { colors, primary_font, secondary_font } = await api(`/sites/${sourceId}/settings/customize`);
const body = JSON.stringify({ colors, primary_font, secondary_font });
const bodyHash = createHash("sha256").update(body).digest("hex");
const sites = await listAll("/sites");

for (const site of sites.filter((candidate) => candidate.id !== sourceId)) {
  await api(`/sites/${site.id}/settings/customize`, {
    method: "PATCH",
    headers: { "Content-Type": "application/json", "Idempotency-Key": `brand-${site.id}-${bodyHash}` },
    body,
  });
  console.log(`Updated ${site.full_domain}`);
}

よくある質問

Notiondesk API ではどの言語を使用できますか?

HTTPSリクエストを送信できる言語であればどれでも使用できます。APIはJSONを使用したシンプルなRESTなので、JavaScript、Python、PHP、Go、Rubyなど、どの言語でも動作します。

これらのスクリプトはどこで実行すれば良いですか?

サーバー上、スケジュールされたジョブ、またはCIパイプライン上で、キーをシークレットとして保存して実行してください。ブラウザ上で実行しないでください。

これらのスクリプトは、APIキーの代わりにOAuthを使用しても構いませんか?

はい。Authorization ヘッダー内の API キーを OAuth アクセス トークンに置き換え、有効期限が切れる前に更新してください。詳しくは、「OAuth を使用してアプリを Notiondesk に接続する」を参照してください。

次は?

関連する記事

このページは役に立ちましたか?

Still need help? Ask us