求人・ライブ配信などのサイトを早くインデックス登録する方法(Google Indexing API)(Node.js)

概要

今回は求人ページやライブ配信ページを、Googleにできるだけ早くインデックス登録する方法について紹介していきます。

求人サイトや動画配信サイトを運営していると、「公開した求人 / 配信ページを1秒でも早くGoogleに拾ってほしい」という悩みはあると思います。

というよりか早く登録しないと死活問題ですよね(^^

求人は応募タイミングが命ですし、ライブ配信は開始時刻を過ぎたら検索結果に出る意味がありません。

しかし通常のクローラ任せだと数時間〜数日かかってしまうので、こういった悩みには対応できません。

ただそこは天下のGoogle様。
こういったページのために即時通知用APIとして「Google Indexing API」というものがあります。

これをNode.jsから叩くように実装をすれば、公開アクションと同時にGoogleに通知して、数分でインデックスに乗せることができます。

今回はそんななるはやでインデックス登録を申請できる方法を紹介していきます。
Node.jsで実装しているので大抵の環境では動くと思います。

それではやっていきましょう(^^!

目次

Indexing APIの対象ページ

公式に対応しているのは以下の2種類のみです。

  • JobPosting:求人情報のページ。schema.org/JobPosting の構造化データが必須
  • BroadcastEventVideoObject 内に埋め込まれたライブ配信イベント。配信開始/終了の通知に使う

なぜOAuth2方式にするのか

認証ルートは2つあります。

  • サービスアカウント方式:GCPで作ったロボットユーザーのJSON鍵で叩く(公式推奨)
  • OAuth2ユーザー認証方式:自分のGoogleアカウントで叩く(要ブラウザ認証)

公式推奨は前者ですが、Search Consoleの「ユーザーと権限」UIはサービスアカウントのメール(xxx@xxx.iam.gserviceaccount.com)を追加できず、ここで詰まります。
レガシーの「プロパティオーナーの管理」画面も標準Search Consoleに統合されてしまい、現状は追加導線が事実上ありません。

OAuth2方式なら自分のGoogleアカウントでAPIを叩くので、SearchConsole側の追加設定は不要です。

  • 初回1回だけブラウザで認証
  • リフレッシュトークンが保存されて、以降は自動で更新
  • Search Console UIの「ユーザーと権限」を触らなくていい

GCP側のセットアップ

プロジェクト作成とIndexing APIの有効化

Google Cloud Consoleの左メニューの「APIとサービス → ライブラリ」から 「Web Search Indexing API」 を有効化します。

OAuthクライアントIDの作成

「APIとサービス → 認証情報 → 認証情報を作成 → OAuthクライアントID」を選択。

  • アプリケーションの種類: デスクトップ アプリ
  • 名前: 任意

作成後、JSON鍵をダウンロードします。

credentials/oauth-client.json

1
2
3
4
5
6
7
8
9
{
"installed": {
"client_id": "xxxxx.apps.googleusercontent.com",
"client_secret": "GOCSPX-xxxxxxxxxxxxxx",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"redirect_uris": ["http://localhost"]
}
}

このJSONは絶対にGitにコミットしないでください。
シークレットキーなんて上げた日には大事故ですよ!
専用フォルダ(例: credentials/)に置いて、フォルダごと .gitignore で除外するのが安全です。

Node.js実装

Node.jsで必要なライブラリは google-auth-library だけインストオールすればOKです!

依存インストール

1
npm install google-auth-library

OAuth2認証フロー(lib/oauth.mjs)

まずは「初回ブラウザ認証 → リフレッシュトークン保存 → 以降は自動更新」の流れです。

実際のソースコードは下記のような形です。

lib/oauth.mjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { OAuth2Client } from 'google-auth-library';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { createServer } from 'node:http';
import { exec } from 'node:child_process';

const TOOL_DIR = resolve(import.meta.dirname, '..');
const CLIENT_FILE = join(TOOL_DIR, 'credentials', 'oauth-client.json');
const TOKEN_FILE = join(TOOL_DIR, 'credentials', 'oauth-token.json');
const SCOPE = 'https://www.googleapis.com/auth/indexing';
const CALLBACK_PORT = 53682;
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/oauth2callback`;

function loadClientConfig() {
const raw = JSON.parse(readFileSync(CLIENT_FILE, 'utf8'));
// GCPダウンロード版は { installed: {...} } でラップされている
return raw.installed ?? raw.web ?? raw;
}

function buildClient() {
const cfg = loadClientConfig();
return new OAuth2Client({
clientId: cfg.client_id,
clientSecret: cfg.client_secret,
redirectUri: REDIRECT_URI,
});
}

async function runAuthFlow() {
const client = buildClient();
const authUrl = client.generateAuthUrl({
access_type: 'offline', // リフレッシュトークン取得に必須
prompt: 'consent', // 毎回同意画面を出してリフレッシュトークン再発行
scope: [SCOPE],
});

const code = await new Promise((resolveCode, rejectCode) => {
const server = createServer((req, res) => {
const reqUrl = new URL(req.url, `http://127.0.0.1:${CALLBACK_PORT}`);
const code = reqUrl.searchParams.get('code');
res.end('<h1>Authenticated. You can close this window.</h1>');
server.close();
code ? resolveCode(code) : rejectCode(new Error('No code'));
});
server.listen(CALLBACK_PORT, '127.0.0.1', () => {
// ブラウザを自動オープン(Windows の例)
exec(`start "" "${authUrl}"`, { shell: 'cmd.exe' });
});
});

const { tokens } = await client.getToken(code);
writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2));
return tokens;
}

export async function getAuthClient() {
const client = buildClient();
const tokens = existsSync(TOKEN_FILE)
? JSON.parse(readFileSync(TOKEN_FILE, 'utf8'))
: await runAuthFlow();
client.setCredentials(tokens);
// アクセストークン更新時に自動保存
client.on('tokens', (newTokens) => {
const merged = { ...tokens, ...newTokens };
writeFileSync(TOKEN_FILE, JSON.stringify(merged, null, 2));
});
return client;
}

ポイントは access_type: 'offline'prompt: 'consent' の組み合わせです。
これがないとリフレッシュトークンが返ってこず、トークン失効のたびに再認証することになります。

Indexing APIを叩く(lib/indexing-api.mjs)

次にアクセストークンを取って urlNotifications:publish にPOSTします。

lib/indexing-api.mjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { getAuthClient } from './oauth.mjs';

const ENDPOINT = 'https://indexing.googleapis.com/v3/urlNotifications:publish';

export async function publishUrls(urls, { type = 'URL_UPDATED' } = {}) {
const client = await getAuthClient();
const { token } = await client.getAccessToken();

const results = await Promise.allSettled(
urls.map(async (url) => {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ url, type }),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
}
return res.json();
})
);

return results.map((r, i) => ({
url: urls[i],
ok: r.status === 'fulfilled',
error: r.status === 'rejected' ? r.reason?.message : null,
}));
}

type には URL_UPDATED(追加・更新通知)と URL_DELETED(削除通知)が指定できます。
求人ページなら新規公開・条件更新時に URL_UPDATED、募集終了時に URL_DELETED
ライブ配信なら配信予定登録時と終了時に同様に呼びます。

エントリポイント

上記で作成した関数を呼び出すのは下記コードで、
引数でURL指定し publishUrls を呼ぶだけです。

notify-index.mjs

1
2
3
4
5
6
7
8
9
10
import { publishUrls } from './lib/indexing-api.mjs';

const args = process.argv.slice(2);
// 例: node notify-index.mjs https://example.com/jobs/12345
const urls = args.filter((a) => a.startsWith('http'));

const results = await publishUrls(urls);
for (const r of results) {
console.log(r.ok ? `OK ${r.url}` : `FAIL ${r.url} -- ${r.error}`);
}

初回認証の実行

実装ができたら、まず認証フローを1回通します。

認証フロー実行

1
node notify-index.mjs --auth

ブラウザでGoogleログイン画面が表示されるのでログインしましょう(^^

「このアプリは確認されていません」警告について

未公開のOAuthアプリなので、必ず「このアプリはGoogleで確認されていません」という警告画面が出ます。
自分で作って自分が使うアプリなので問題ありません。
画面下の「詳細」→「プロジェクト名(安全ではないページ)に移動」で進み、スコープ確認画面で 続行 すれば完了です。

ブラウザに Authenticated と表示されたらタブを閉じてOKです。
ターミナルに Saved tokens to ...oauth-token.json と出れば成功です。

実行と動作確認

認証完了後、インデックス登録を行いたいURLを引数に下記のように実行します。

インデックス通知

1
node notify-index.mjs https://example.com/jobs/12345

成功すると以下のような出力になります。

1
OK   https://example.com/jobs/12345

数分後にSearch ConsoleのURL検査ツールで対象URLを確認して、「最終クロール日時」が更新されていれば成功です。

よくある失敗パターンと対処

  • 403 Permission denied. Failed to verify the URL ownership.:認証アカウントがSearch Consoleのオーナーになっていない
  • 403 Indexing API has not been used in project:GCP側でAPI有効化忘れ
  • No refresh_token returnedprompt: 'consent' 抜け、または既に同アプリが認可済み。Googleアカウント権限管理でアプリを削除してから再認証
  • access_blocked:OAuth同意画面の「テストユーザー」に自分のメアドが入っていない

まとめ

要点は下記のような感じです!

  • Google Indexing APIの対象はJobPosting / BroadcastEventページのみ
  • 認証はOAuth2ユーザー認証方式が現状確実
  • google-auth-libraryだけで実装可能、
  • リフレッシュトークン保存で初回以降は完全自動
  • 公開時URL_UPDATED/終了時URL_DELETEDでパラメータの使い分けが必要

毎回URLを登録するのはかなり面倒なので、こんな感じで自動化のAPIがあると便利ですよね!

自分のブログでは他にも実務レベルの知識を取り扱っております。
良ければ他の記事も見ていってください!

以上となります!
自動化は生きていくうえで最重要事項ですね(^^b
それではお疲れさまでした!!