この記事では、チームが最も自動化するタスク(CIパイプラインからのNotion編集の公開、すべての記事のMarkdown形式でのバックアップ、Slackへの否定的なフィードバックの送信、ヘルプセンターで回答できない検索の報告、同期失敗の検出、複数のヘルプセンターへの1つのブランドの適用など)に対応した、すぐに使えるNotiondesk APIスクリプトをまとめています。レシピをコピーし、2つの環境変数を設定して実行してください。
始める前に
- レシピに必要なスコープのみを持つ API キーを作成します。各レシピは、これらのスコープに名前を付けます。詳細については、「API キーを使用して Notiondesk API で認証する」を参照してください。
- ヘルプセンターの
idをGET /sitesで検索します。 Notiondesk API を使用してヘルプセンターを読み取る方法を参照してください。
- 両方を環境変数として設定します。
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 回何もキューに入れません。
すべての記事をマークダウン形式でバックアップする
範囲: 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, "&").replace(/</g, "<").replace(/>/g, ">");
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")}` }),
});
}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 に接続する」を参照してください。