コードの組み込み
サイトの開発担当者向け。匿名の埋め込みの 1 行の script、会員認証の埋め込みの入場チケットと会員情報、ウィジェットの言語、ヘルプ記事の自動翻訳
このページはサイトを担当する開発者向けです。管理画面からコピーした埋め込みコードとコードの断片を、サイトのどこに置き、どう変更するかを説明します。 サイトの URL は、先に埋め込み先サイトに登録しておく必要があります。登録していないサイトでは、埋め込みコードを置いても使えません。
2 種類の埋め込みの違いは、バックエンドが必要かどうか
- 匿名の埋め込み —— ページに
<script>を 1 行加えるだけです。静的なページでも使えます - 会員認証の埋め込み —— 会員がログインしたあと、サイトのバックエンドで「入場チケット」を署名し、その 1 行の
<script>に入れます。会員ログインと会員情報がすでにあるサイト向けです
以下の例の https://your-domain.example.com は SupaTalker の管理画面の URL を表します。Host ID と secret も例の値なので、必ず管理画面からコピーしたものを使ってください。
匿名の埋め込み
管理画面の「匿名の埋め込み」エリアで URL を登録したら、「埋め込みコードをコピー」を押し、チャットウィジェットを表示したいすべてのページの </body> の直前にこの 1 行を貼り付けます。
<!doctype html>
<html lang="ja">
<body>
<!-- サイトの既存の内容 -->
<script src="https://your-domain.example.com/embed.js"></script>
</body>
</html>ページの右下に吹き出しボタンが表示され、訪問者が押すとチャットウィジェットが開きます。以下は SupaTalker の公式サイトに実際に埋め込んだ様子です。


吹き出しの色、形、位置、名前は管理画面の全体設定でまとめて決めるため、この 1 行に属性を追加する必要はありません。管理画面で変更すると約 1 分で反映され、貼り直す必要もありません。
会員認証の埋め込み
会員ログインの仕組みがあり、Node、PHP、Python などのバックエンド言語で作られたサイト向けです。仕組みは次のとおりです。
- 会員がサイトにログインしたら、サイトのバックエンドが Host ID と secret で入場チケットを署名します
- 入場チケットを
<script>のdata-member-token属性に入れて、ページに出力します - チャットウィジェットはこのチケットで会員の本人確認を行い、訪問者は会員として会話します。会話の履歴も会員ごとに保存されます
管理画面の「会員認証の埋め込み」エリアでサイトを作成すると、そのサイトの「Node のコードをコピー」「PHP のコードをコピー」「Python のコードをコピー」で、Host ID と secret が入力済みのコードの断片をコピーできます。内容は次のとおりです。
const crypto = require('node:crypto')
const appOrigin = "https://your-domain.example.com"
const hostId = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
const secret = "<secret>"
const base64url = (value) => Buffer.from(value).toString('base64url')
const now = Math.floor(Date.now() / 1000)
const payload = { v: 1, iss: hostId, sub: currentUser.id, jti: crypto.randomUUID(), name: currentUser.name, email: currentUser.email, iat: now, exp: now + 120 }
const signingInput = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) + '.' + base64url(JSON.stringify(payload))
const token = signingInput + '.' + crypto.createHmac('sha256', secret).update(signingInput).digest('base64url')
res.setHeader('Cache-Control', 'private, no-store')
res.write('<script src="' + appOrigin + '/embed.js" data-member-token="' + token + '"><\/script>')<?php
$appOrigin = "https://your-domain.example.com";
$hostId = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
$secret = "<secret>";
$base64url = fn($value) => rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
$now = time();
$payload = ['v' => 1, 'iss' => $hostId, 'sub' => $currentUser['id'], 'jti' => bin2hex(random_bytes(16)), 'name' => $currentUser['name'], 'email' => $currentUser['email'], 'iat' => $now, 'exp' => $now + 120];
$signingInput = $base64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])) . '.' . $base64url(json_encode($payload));
$token = $signingInput . '.' . $base64url(hash_hmac('sha256', $signingInput, $secret, true));
header('Cache-Control: private, no-store');
echo '<script src="' . htmlspecialchars($appOrigin, ENT_QUOTES) . '/embed.js" data-member-token="' . htmlspecialchars($token, ENT_QUOTES) . '"><\/script>';import base64, hashlib, hmac, json, time, uuid
app_origin = "https://your-domain.example.com"
host_id = "embed_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
secret = "<secret>"
def base64url(value): return base64.urlsafe_b64encode(value).rstrip(b'=').decode()
now = int(time.time())
payload = {'v': 1, 'iss': host_id, 'sub': current_user.id, 'jti': str(uuid.uuid4()), 'name': current_user.name, 'email': current_user.email, 'iat': now, 'exp': now + 120}
signing_input = base64url(json.dumps({'alg': 'HS256', 'typ': 'JWT'}, separators=(',', ':')).encode()) + '.' + base64url(json.dumps(payload, separators=(',', ':')).encode())
signature = base64url(hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).digest())
token = signing_input + '.' + signature
response.headers['Cache-Control'] = 'private, no-store'
response.write('<script src="' + app_origin + '/embed.js" data-member-token="' + token + '"><\/script>')コードの断片のうち、次の変数はサイト側のものに置き換えます。
| 断片の中の名前 | 置き換えるもの |
|---|---|
currentUser(Python は current_user、PHP は $currentUser) | 現在ログインしている会員。次の節を参照してください |
res/response | サイトのフレームワークのレスポンスオブジェクト |
res.write(...)/echo/response.write(...) | この <script> をページの </body> の直前に出力するように変更します。たとえばページのテンプレートに入れます |
secret はサイトのバックエンドにだけ置いてください
secret はこのサイトの鍵にあたるもので、ブラウザから見える場所(フロントエンドの JavaScript、HTML のソース)に置いてはいけません。バージョン管理にも含めないでください。 環境変数から読み込むことをおすすめします。secret が漏れた場合は、管理画面で「secret をローテーション」を押して新しい secret に切り替え、サイト側も更新してください。
会員情報の渡し方
会員情報は入場チケットの内容(上の断片の payload)に入れます。
| 項目 | 必須 | 説明 |
|---|---|---|
v | ✓ | 1 で固定 |
iss | ✓ | 管理画面に表示されている Host ID |
sub | ✓ | サイト上での会員の変わらない ID。英字または数字で始まり、英数字と . _ : - のみ使えます。最大 128 文字 |
jti | ✓ | チケットごとに異なるランダムな値。断片の中ですでに生成されています |
iat、exp | ✓ | 発行日時と有効期限(Unix 秒)。有効期限は発行から最大 120 秒後までです |
name | 任意 | 会員の氏名。最大 120 文字 |
email | 任意 | 会員のメールアドレス。最大 254 文字 |
subで「同じ会員かどうか」が決まります:同じsubは同じ会員とみなされ、会話の履歴はすべて同じ訪問者にまとめられます。データベースの会員番号を使い、メールアドレスは使わないでください(@を含むため形式に合わず、会員がメールアドレスを変えることもあります)name、emailは一度だけ書き込まれます:管理画面の訪問者の情報でこの 2 つの項目がまだ空のときだけ入力されます。あとでサイト側で変えても、管理画面の既存の値は上書きされません- ページを読み込むたびに署名し直す必要があります:チケットは 1 回しか使えず、120 秒で期限切れになります。そのため、この
<script>はページを生成するたびにその場で署名する必要があり、キャッシュされてはいけません。断片の中のCache-Control: private, no-storeはそのための設定です
Node.js(Express)の例として、断片を関数にまとめ、会員がログインしたあとのページで出力します。
const crypto = require('node:crypto')
const appOrigin = 'https://your-domain.example.com'
const hostId = process.env.SUPATALKER_HOST_ID
const secret = process.env.SUPATALKER_EMBED_SECRET
const base64url = (value) => Buffer.from(value).toString('base64url')
function memberChatScript(member) {
const now = Math.floor(Date.now() / 1000)
const payload = {
v: 1,
iss: hostId,
sub: String(member.id),
jti: crypto.randomUUID(),
name: member.name,
email: member.email,
iat: now,
exp: now + 120,
}
const signingInput =
base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) +
'.' +
base64url(JSON.stringify(payload))
const token =
signingInput + '.' + crypto.createHmac('sha256', secret).update(signingInput).digest('base64url')
return `<script src="${appOrigin}/embed.js" data-member-token="${token}"></script>`
}
app.get('/account', requireLogin, (req, res) => {
res.setHeader('Cache-Control', 'private, no-store')
// req.user はサイト独自のログインの仕組みが提供する会員情報
res.render('account', { chatScript: memberChatScript(req.user) })
})ページのテンプレートで、</body> の直前に chatScript を出力します(テンプレートの書き方に従い、文字としてエスケープされないようにしてください)。
会員がログインしていないページ
会員認証のエリアに登録した URL では、匿名のチャットもそのまま埋め込めるため、別に登録する必要はありません。会員がまだログインしていないときは匿名の埋め込みの 1 行を出力し、ログイン後に data-member-token 付きのものに切り替えてください。
ウィジェットの言語
チャットウィジェットの画面と AI の返信は、繁體中文、English、日本語の 3 つに対応しており、次の順番で決まります。
<script>のdata-locale属性- ページの
<html lang>の値 - 管理画面でその埋め込み先サイトに設定した「チャットウィジェットの既定の言語」
- 全体設定のサイトのデフォルト言語
最初の 2 つでは、zh-TW、zh-Hant、zh-HK は繁体字中国語、en、en-US などは英語、ja は日本語とみなされます。それ以外の言語(簡体字中国語を含む)の場合は、次の項目を見ます。
サイト自体が多言語対応なら、通常は追加の設定は不要で、ウィジェットは <html lang> に従います。シングルページアプリケーションで言語を切り替えたときに <html lang> だけが変わる場合も、ウィジェットはそれに合わせて切り替わります。言語を固定したい場合は、<script> に data-locale を加えます。
<script src="https://your-domain.example.com/embed.js" data-locale="ja"></script>data-locale を使うと、ウィジェットは <html lang> の変化に従わなくなります。
下の画像は、SupaTalker の公式サイトを日本語に切り替えてウィジェットを開いたところです。画面が日本語になり、人気の質問も日本語の訳文で表示され、右上に「自動翻訳」と表示されています(次の節を参照)。

その他:ヘルプ記事の自動翻訳
ウィジェットの「ヘルプ」タブとホームの「人気の質問」に表示される記事は、全体設定でチェックを入れたナレッジベースのカテゴリから取得されます。 これらの記事は 1 つの言語で書くだけで、システムがウィジェットの対応するほかの 2 つの言語に自動で翻訳します。

- 元の言語はサイトのデフォルト言語で、ほかの 2 つの言語に自動で翻訳されます
- ウィジェットがほかの言語で表示されるときは、記事のタイトルと回答が訳文になります。ホームの人気の質問には「自動翻訳」と表示され、ヘルプタブで展開した回答の下には「自動翻訳 · 原文を表示」と表示されます。訪問者は「原文を表示」で原文に切り替えられます
- 翻訳は 1 時間に 1 回(毎時 30 分)で、新しく追加された記事や変更された記事だけを翻訳します。そのため、新しい記事や変更したばかりの記事は、次の翻訳が終わるまで原文で表示されます
- 翻訳に失敗した記事は原文のまま表示され、セキュリティルールと監査にイベントが 1 件記録されます
翻訳されるのは、チェックを入れたカテゴリ内の公開済みのナレッジベースの記事だけです。会話での AI の返信はここの影響を受けません。AI はもともとウィジェットの現在の言語で回答します。