@squadbase/vite-server 0.1.9-dev.f236b23 → 0.1.10-dev.5b0c0a8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,131 @@ var ParameterDefinition = class {
42
42
  }
43
43
  };
44
44
 
45
+ // ../connectors/src/lib/ssh-tunnel.ts
46
+ var sshTunnelParameters = {
47
+ sshHost: new ParameterDefinition({
48
+ slug: "ssh-host",
49
+ name: "SSH Tunnel Host",
50
+ description: "Optional. Hostname of the SSH bastion to tunnel through. Leave empty to connect directly.",
51
+ envVarBaseKey: "SSH_TUNNEL_HOST",
52
+ type: "text",
53
+ secret: false,
54
+ required: false
55
+ }),
56
+ sshPort: new ParameterDefinition({
57
+ slug: "ssh-port",
58
+ name: "SSH Tunnel Port",
59
+ description: "Optional. SSH port of the bastion host (default: 22).",
60
+ envVarBaseKey: "SSH_TUNNEL_PORT",
61
+ type: "text",
62
+ secret: false,
63
+ required: false
64
+ }),
65
+ sshUsername: new ParameterDefinition({
66
+ slug: "ssh-username",
67
+ name: "SSH Tunnel Username",
68
+ description: "Optional. Username for SSH authentication. Required when SSH Tunnel Host is set.",
69
+ envVarBaseKey: "SSH_TUNNEL_USERNAME",
70
+ type: "text",
71
+ secret: false,
72
+ required: false
73
+ }),
74
+ sshPrivateKeyBase64: new ParameterDefinition({
75
+ slug: "ssh-private-key-base64",
76
+ name: "SSH Private Key",
77
+ description: "Optional. Private key (PEM, base64-encoded) used for SSH authentication. Required when SSH Tunnel Host is set.",
78
+ envVarBaseKey: "SSH_TUNNEL_PRIVATE_KEY_BASE64",
79
+ type: "base64EncodedText",
80
+ secret: true,
81
+ required: false
82
+ }),
83
+ sshPassphrase: new ParameterDefinition({
84
+ slug: "ssh-passphrase",
85
+ name: "SSH Private Key Passphrase",
86
+ description: "Optional. Passphrase for the SSH private key, if it is encrypted.",
87
+ envVarBaseKey: "SSH_TUNNEL_PASSPHRASE",
88
+ type: "text",
89
+ secret: true,
90
+ required: false
91
+ })
92
+ };
93
+ var NOOP_TUNNEL_HOSTPORT = (host, port) => ({
94
+ host,
95
+ port,
96
+ close: async () => {
97
+ }
98
+ });
99
+ function connectionParamsToRecord(connection2) {
100
+ const out = {};
101
+ for (const p of connection2.parameters) {
102
+ if (p.value != null) out[p.parameterSlug] = p.value;
103
+ }
104
+ return out;
105
+ }
106
+ async function maybeOpenSshTunnelHostPort(params, dbHost, dbPort) {
107
+ const sshHost = params[sshTunnelParameters.sshHost.slug];
108
+ if (!sshHost) return NOOP_TUNNEL_HOSTPORT(dbHost, dbPort);
109
+ const sshUsername = params[sshTunnelParameters.sshUsername.slug];
110
+ const sshPrivateKeyBase64 = params[sshTunnelParameters.sshPrivateKeyBase64.slug];
111
+ if (!sshUsername || !sshPrivateKeyBase64) {
112
+ throw new Error(
113
+ "SSH tunnel requires `ssh-username` and `ssh-private-key-base64` when `ssh-host` is set."
114
+ );
115
+ }
116
+ const sshPort = Number(params[sshTunnelParameters.sshPort.slug] || "22") || 22;
117
+ const sshPassphrase = params[sshTunnelParameters.sshPassphrase.slug];
118
+ const [{ Client }, net] = await Promise.all([
119
+ import("ssh2"),
120
+ import("net")
121
+ ]);
122
+ const sshClient = new Client();
123
+ await new Promise((resolve, reject) => {
124
+ sshClient.once("ready", () => resolve());
125
+ sshClient.once("error", reject);
126
+ sshClient.connect({
127
+ host: sshHost,
128
+ port: sshPort,
129
+ username: sshUsername,
130
+ privateKey: Buffer.from(sshPrivateKeyBase64, "base64"),
131
+ passphrase: sshPassphrase || void 0,
132
+ readyTimeout: 1e4
133
+ });
134
+ });
135
+ const server = net.createServer((socket) => {
136
+ sshClient.forwardOut(
137
+ socket.remoteAddress ?? "127.0.0.1",
138
+ socket.remotePort ?? 0,
139
+ dbHost,
140
+ dbPort,
141
+ (err, stream) => {
142
+ if (err) {
143
+ socket.destroy(err);
144
+ return;
145
+ }
146
+ socket.pipe(stream).pipe(socket);
147
+ }
148
+ );
149
+ });
150
+ await new Promise((resolve, reject) => {
151
+ server.once("error", reject);
152
+ server.listen(0, "127.0.0.1", () => resolve());
153
+ });
154
+ const address = server.address();
155
+ if (!address || typeof address === "string") {
156
+ server.close();
157
+ sshClient.end();
158
+ throw new Error("Failed to allocate local port for SSH tunnel.");
159
+ }
160
+ return {
161
+ host: "127.0.0.1",
162
+ port: address.port,
163
+ close: async () => {
164
+ await new Promise((resolve) => server.close(() => resolve()));
165
+ sshClient.end();
166
+ }
167
+ };
168
+ }
169
+
45
170
  // ../connectors/src/connectors/oracle/utils.ts
46
171
  var JDBC_THIN_PREFIX_RE = /^jdbc:oracle:thin:/i;
47
172
  var JDBC_OCI_PREFIX_RE = /^jdbc:oracle:oci/i;
@@ -107,35 +232,76 @@ function parseOracleJdbcUrl(jdbcUrl, options = {}) {
107
232
  function redactOracleUrl(jdbcUrl) {
108
233
  return jdbcUrl.replace(/(:\/\/)([^@/]+)@/, "$1***@").replace(/(thin:)([^@]+)@/i, "$1***@");
109
234
  }
235
+ function parseOracleConnectStringHostPort(connectString) {
236
+ const m = /^([^:/]+):(\d+)(.*)$/.exec(connectString);
237
+ if (!m) return null;
238
+ return { host: m[1], port: Number(m[2]), trailing: m[3] };
239
+ }
240
+ function rewriteOracleConnectStringHostPort(connectString, host, port) {
241
+ const parts = parseOracleConnectStringHostPort(connectString);
242
+ if (!parts) return connectString;
243
+ return `${host}:${port}${parts.trailing}`;
244
+ }
110
245
 
111
246
  // ../connectors/src/lib/oracle-runner.ts
247
+ var GLOBAL_ORACLEDB_PATH = "/usr/lib/node_modules/oracledb/index.js";
112
248
  async function importOracleDb() {
113
- const mod = await import("oracledb");
114
- return mod.default ?? mod;
249
+ let mod;
250
+ try {
251
+ mod = await import("oracledb");
252
+ } catch {
253
+ mod = await import(GLOBAL_ORACLEDB_PATH);
254
+ }
255
+ const resolved = mod;
256
+ return resolved.default ?? resolved;
115
257
  }
116
- async function runOracleQuery(parsed, sql) {
258
+ async function runOracleQuery(parsed, sql, options = {}) {
117
259
  const oracledb = await importOracleDb();
118
- const connection2 = await oracledb.getConnection({
119
- user: parsed.user,
120
- password: parsed.password,
121
- connectString: parsed.connectString
122
- });
260
+ let tunnel = null;
261
+ if (options.tunnelParams) {
262
+ const hostPort = parseOracleConnectStringHostPort(parsed.connectString);
263
+ if (hostPort) {
264
+ tunnel = await maybeOpenSshTunnelHostPort(
265
+ options.tunnelParams,
266
+ hostPort.host,
267
+ hostPort.port
268
+ );
269
+ }
270
+ }
123
271
  try {
124
- const result = await connection2.execute(sql, [], {
125
- outFormat: oracledb.OUT_FORMAT_OBJECT,
126
- // Bound by the connector's own row cap, but keep the driver from
127
- // streaming arbitrarily large result sets.
128
- maxRows: 5e3
272
+ const connectString = tunnel ? rewriteOracleConnectStringHostPort(
273
+ parsed.connectString,
274
+ tunnel.host,
275
+ tunnel.port
276
+ ) : parsed.connectString;
277
+ const connection2 = await oracledb.getConnection({
278
+ user: parsed.user,
279
+ password: parsed.password,
280
+ connectString
129
281
  });
130
- return { rows: result.rows ?? [] };
131
- } finally {
132
282
  try {
133
- await connection2.close();
134
- } catch {
283
+ const result = await connection2.execute(
284
+ sql,
285
+ [],
286
+ {
287
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
288
+ // Bound by the connector's own row cap, but keep the driver from
289
+ // streaming arbitrarily large result sets.
290
+ maxRows: 5e3
291
+ }
292
+ );
293
+ return { rows: result.rows ?? [] };
294
+ } finally {
295
+ try {
296
+ await connection2.close();
297
+ } catch {
298
+ }
135
299
  }
300
+ } finally {
301
+ await tunnel?.close();
136
302
  }
137
303
  }
138
- async function checkOracleConnection(url, credentials) {
304
+ async function checkOracleConnection(url, credentials, options = {}) {
139
305
  let parsed;
140
306
  try {
141
307
  parsed = parseOracleJdbcUrl(url, credentials);
@@ -146,7 +312,7 @@ async function checkOracleConnection(url, credentials) {
146
312
  };
147
313
  }
148
314
  try {
149
- await runOracleQuery(parsed, "SELECT 1 FROM DUAL");
315
+ await runOracleQuery(parsed, "SELECT 1 FROM DUAL", options);
150
316
  return { success: true };
151
317
  } catch (err) {
152
318
  let msg = err instanceof Error ? err.message : String(err);
@@ -183,7 +349,8 @@ var parameters = {
183
349
  type: "text",
184
350
  secret: true,
185
351
  required: false
186
- })
352
+ }),
353
+ ...sshTunnelParameters
187
354
  };
188
355
 
189
356
  // ../connectors/src/connectors/oracle/sdk/index.ts
@@ -200,7 +367,9 @@ function createClient(params) {
200
367
  async function runQuery(sql) {
201
368
  try {
202
369
  const cleanSql = sql.replace(/;\s*$/, "");
203
- const { rows } = await runOracleQuery(parsed, cleanSql);
370
+ const { rows } = await runOracleQuery(parsed, cleanSql, {
371
+ tunnelParams: params
372
+ });
204
373
  return rows;
205
374
  } catch (err) {
206
375
  const msg = err instanceof Error ? err.message : String(err);
@@ -449,7 +618,9 @@ Do NOT terminate statements with a semicolon; the driver rejects trailing termin
449
618
  }
450
619
  try {
451
620
  const cleanSql = sql.replace(/;\s*$/, "");
452
- const { rows } = await runOracleQuery(parsed, cleanSql);
621
+ const { rows } = await runOracleQuery(parsed, cleanSql, {
622
+ tunnelParams: connectionParamsToRecord(connection2)
623
+ });
453
624
  const truncated = rows.length > MAX_ROWS;
454
625
  return {
455
626
  success: true,
@@ -519,10 +690,14 @@ The business logic type for this connector is "sql".
519
690
  },
520
691
  tools,
521
692
  async checkConnection(params, _config) {
522
- return checkOracleConnection(params[parameters.jdbcUrl.slug], {
523
- username: params[parameters.username.slug],
524
- password: params[parameters.password.slug]
525
- });
693
+ return checkOracleConnection(
694
+ params[parameters.jdbcUrl.slug],
695
+ {
696
+ username: params[parameters.username.slug],
697
+ password: params[parameters.password.slug]
698
+ },
699
+ { tunnelParams: params }
700
+ );
526
701
  },
527
702
  async query(params, sql, _namedParams) {
528
703
  const parsed = parseOracleJdbcUrl(params[parameters.jdbcUrl.slug], {
@@ -532,11 +707,13 @@ The business logic type for this connector is "sql".
532
707
  const sample = unwrapSampleLimit(sql);
533
708
  if (sample) {
534
709
  const inner = sample.inner.replace(/;\s*$/, "");
535
- const result = await runOracleQuery(parsed, inner);
710
+ const result = await runOracleQuery(parsed, inner, {
711
+ tunnelParams: params
712
+ });
536
713
  return { rows: result.rows.slice(0, sample.limit) };
537
714
  }
538
715
  const cleanSql = sql.replace(/;\s*$/, "");
539
- return runOracleQuery(parsed, cleanSql);
716
+ return runOracleQuery(parsed, cleanSql, { tunnelParams: params });
540
717
  }
541
718
  });
542
719
 
@@ -97,13 +97,41 @@ function createClient(params) {
97
97
  const { query, ...rest } = init ?? {};
98
98
  return fetch(buildUrl(path2, query), rest);
99
99
  }
100
+ async function checkUnits() {
101
+ const url = new URL("https://www.semrush.com/users/countapiunits.html");
102
+ url.searchParams.set("key", apiKey);
103
+ const res = await fetch(url.toString(), { method: "GET" });
104
+ const text = (await res.text()).trim();
105
+ if (text.startsWith("ERROR ")) {
106
+ throw new Error(`semrush checkUnits: ${text}`);
107
+ }
108
+ if (!res.ok) {
109
+ throw new Error(
110
+ `semrush checkUnits: ${res.status} ${res.statusText}: ${text}`
111
+ );
112
+ }
113
+ const n = Number(text);
114
+ if (!Number.isFinite(n)) {
115
+ throw new Error(
116
+ `semrush checkUnits: response is not numeric: ${text.slice(0, 200)}`
117
+ );
118
+ }
119
+ return n;
120
+ }
100
121
  return {
101
122
  request,
123
+ checkUnits,
102
124
  async report(type, query) {
103
125
  const res = await request("/", { query: { type, ...query ?? {} } });
104
126
  const text = await res.text();
105
127
  if (text.startsWith("ERROR ")) {
106
- throw new Error(`semrush: ${text.trim()}`);
128
+ const trimmed = text.trim();
129
+ if (/API UNITS BALANCE IS ZERO/i.test(trimmed)) {
130
+ throw new Error(
131
+ `semrush: ${trimmed}. The Semrush account has no remaining API units. Top up units in the Semrush console or call checkUnits() before report() to fail fast.`
132
+ );
133
+ }
134
+ throw new Error(`semrush: ${trimmed}`);
107
135
  }
108
136
  if (!res.ok) {
109
137
  throw new Error(
@@ -468,16 +496,18 @@ var semrushOnboarding = new ConnectorOnboarding({
468
496
  - Write only 1 sentence between tool calls, then immediately call the next tool. Skip unnecessary explanations and proceed efficiently`
469
497
  },
470
498
  dataOverviewInstructions: {
471
- en: `1. Call ${requestToolName} with path "/" and queryParams \`{ "type": "domain_overview", "domain": "<example.com>", "database": "us" }\` to inspect the domain overview report (CSV)
499
+ en: `1. Call ${requestToolName} with path "/" and queryParams \`{ "type": "domain_ranks", "domain": "<example.com>", "database": "us" }\` to inspect the domain summary report (single-row CSV: rank, organic/paid keywords, traffic, cost)
472
500
  2. Call ${requestToolName} with path "/" and queryParams \`{ "type": "domain_organic", "domain": "<example.com>", "database": "us", "display_limit": "5" }\` to sample organic keywords
473
501
  3. Call ${requestToolName} with path "/" and queryParams \`{ "type": "phrase_this", "phrase": "<keyword>", "database": "us" }\` to inspect a keyword overview
474
- 4. Explore other report types (backlinks_overview, domain_adwords, phrase_related) and the Trends API ("/analytics/v1/...") with responseFormat="json" as needed
475
- 5. Remember: the Standard Analytics API returns semicolon-separated CSV with the first row as the header`,
476
- ja: `1. ${requestToolName} \u3067 path "/" \u3068 queryParams \`{ "type": "domain_overview", "domain": "<example.com>", "database": "us" }\` \u3092\u547C\u3073\u51FA\u3057\u3001\u30C9\u30E1\u30A4\u30F3\u30AA\u30FC\u30D0\u30FC\u30D3\u30E5\u30FC\uFF08CSV\uFF09\u3092\u78BA\u8A8D
502
+ 4. Optionally call ${requestToolName} with path "/analytics/v1/" and queryParams \`{ "type": "backlinks_overview", "target": "<example.com>", "target_type": "root_domain" }\` to inspect a backlinks summary (returns semicolon-separated CSV \u2014 keep \`responseFormat="text"\`)
503
+ 5. Always pass an explicit small \`display_limit\` (e.g. \`"5"\`) \u2014 never rely on the default 10000, which can trip \`ERROR 132\` even when units remain
504
+ 6. Remember: the Standard Analytics API and the \`/analytics/v1/?type=backlinks_*\` Backlinks endpoints both return semicolon-separated CSV with the first row as the header`,
505
+ ja: `1. ${requestToolName} \u3067 path "/" \u3068 queryParams \`{ "type": "domain_ranks", "domain": "<example.com>", "database": "us" }\` \u3092\u547C\u3073\u51FA\u3057\u3001\u30C9\u30E1\u30A4\u30F3\u30B5\u30DE\u30EA\u30FC\u30EC\u30DD\u30FC\u30C8\uFF08rank\u30FB\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF/\u6709\u6599\u30AD\u30FC\u30EF\u30FC\u30C9\u30FB\u30C8\u30E9\u30D5\u30A3\u30C3\u30AF\u30FB\u30B3\u30B9\u30C8\u7B49\u306E1\u884CCSV\uFF09\u3092\u78BA\u8A8D
477
506
  2. ${requestToolName} \u3067 path "/" \u3068 queryParams \`{ "type": "domain_organic", "domain": "<example.com>", "database": "us", "display_limit": "5" }\` \u3092\u547C\u3073\u51FA\u3057\u3001\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF\u30AD\u30FC\u30EF\u30FC\u30C9\u3092\u30B5\u30F3\u30D7\u30EA\u30F3\u30B0
478
507
  3. ${requestToolName} \u3067 path "/" \u3068 queryParams \`{ "type": "phrase_this", "phrase": "<keyword>", "database": "us" }\` \u3092\u547C\u3073\u51FA\u3057\u3001\u30AD\u30FC\u30EF\u30FC\u30C9\u6982\u8981\u3092\u78BA\u8A8D
479
- 4. \u5FC5\u8981\u306B\u5FDC\u3058\u3066\u4ED6\u306E\u30EC\u30DD\u30FC\u30C8\u30BF\u30A4\u30D7\uFF08backlinks_overview\u3001domain_adwords\u3001phrase_related\uFF09\u3084 Trends API ("/analytics/v1/...", responseFormat="json") \u3092\u63A2\u7D22
480
- 5. \u6CE8\u610F: Standard Analytics API \u306F\u30BB\u30DF\u30B3\u30ED\u30F3\u533A\u5207\u308ACSV\u3092\u8FD4\u3057\u30011\u884C\u76EE\u304C\u30D8\u30C3\u30C0\u30FC`
508
+ 4. \u5FC5\u8981\u306B\u5FDC\u3058\u3066 ${requestToolName} \u3067 path "/analytics/v1/" \u3068 queryParams \`{ "type": "backlinks_overview", "target": "<example.com>", "target_type": "root_domain" }\` \u3092\u547C\u3073\u51FA\u3057\u3066\u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u30B5\u30DE\u30EA\u30FC\u3092\u78BA\u8A8D\uFF08\u30BB\u30DF\u30B3\u30ED\u30F3\u533A\u5207\u308ACSV\u3092\u8FD4\u3059\u305F\u3081 \`responseFormat="text"\` \u306E\u307E\u307E\uFF09
509
+ 5. \`display_limit\` \u306F\u5FC5\u305A\u660E\u793A\u7684\u306B\u5C0F\u3055\u3044\u5024\uFF08\u4F8B: \`"5"\`\uFF09\u3092\u6307\u5B9A\u3059\u308B\u3053\u3068\u3002\u30C7\u30D5\u30A9\u30EB\u30C810000\u306E\u307E\u307E\u3060\u3068\u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u304C\u3042\u3063\u3066\u3082 \`ERROR 132\` \u3067\u62D2\u5426\u3055\u308C\u308B\u3053\u3068\u304C\u3042\u308B
510
+ 6. \u6CE8\u610F: Standard Analytics API \u304A\u3088\u3073 \`/analytics/v1/?type=backlinks_*\` \u306E\u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u7CFB\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306F\u3069\u3061\u3089\u3082\u30BB\u30DF\u30B3\u30ED\u30F3\u533A\u5207\u308ACSV\uFF081\u884C\u76EE\u304C\u30D8\u30C3\u30C0\u30FC\uFF09\u3092\u8FD4\u3059`
481
511
  }
482
512
  });
483
513
 
@@ -504,10 +534,19 @@ The business logic type for this connector is "typescript". Write handler code u
504
534
 
505
535
  SDK methods (client created via \`connection(connectionId)\`):
506
536
  - \`client.request(path, init?)\` \u2014 low-level authenticated fetch. Pass \`init.query\` to set query parameters; \`key\` is injected automatically
507
- - \`client.report(type, query?)\` \u2014 call a Standard Analytics report and parse the CSV into \`{ columns, rows, raw }\`
537
+ - \`client.checkUnits()\` \u2014 return the current API unit balance as a number. **FREE \u2014 does not consume units.** Use as a pre-flight before \`report()\`
538
+ - \`client.report(type, query?)\` \u2014 call a Standard Analytics report and parse the CSV into \`{ columns: string[], rows: Record<string, string>[], raw: string }\`. Throws \`API UNITS BALANCE IS ZERO\` when the account is out of units
508
539
  - \`client.trends(path, query?)\` \u2014 call a Trends API endpoint (\`/analytics/v1/...\`) and return parsed JSON
509
540
  - \`client.projects(path, init?)\` \u2014 call a Projects API endpoint (\`/management/v1/...\`) and return parsed JSON
510
541
 
542
+ \u{1F6A8} **MANDATORY pre-flight before creating any server logic that calls \`report()\`.** Before authoring a TypeScript handler that invokes \`client.report(...)\` (Standard Analytics \u2014 \`domain_ranks\`, \`domain_organic\`, \`phrase_this\`, etc.), you MUST first call the \`semrush-api-key_request\` tool with \`path: "https://www.semrush.com/users/countapiunits.html"\`, no \`queryParams\`, \`responseFormat: "text"\` to confirm the unit balance is greater than 0. If the balance is 0, **stop, do not create the server logic**, and report the issue to the user (the account owner needs to top up API units in the Semrush console). \`testFetchServerLogic\` will execute the handler and consume units; creating handlers when the balance is 0 produces \`ERROR 132 :: API UNITS BALANCE IS ZERO\` failures across every server logic.
543
+
544
+ \u{1F6A8} **Always set a small \`display_limit\` explicitly \u2014 never rely on the default.** Semrush computes "required units = display_limit \xD7 per-row cost" up front, and rejects the request with \`ERROR 132 :: API UNITS BALANCE IS ZERO\` when the *estimated* cost exceeds the remaining balance, **even when the actual balance is non-zero** (e.g. balance \u2248 40,950 still fails for \`display_limit=10000\` on costly reports). Default to \`display_limit: "1000"\` (or smaller \u2014 \`"100"\` is plenty for most dashboards), and only increase it when the user explicitly asks for a larger sample AND \`checkUnits()\` confirms enough headroom. If you truly need all rows, paginate with \`display_offset\` in chunks of 1000 instead of bumping \`display_limit\`. Treat \`ERROR 132\` as "request too large for the current balance" first, and only as "balance is literally zero" after re-checking units.
545
+
546
+ \u{1F6AB} **Do NOT use the Trends API (\`client.trends\`) or Projects API (\`client.projects\`) unless the user explicitly requests them.** These APIs require separate paid subscriptions (Trends and Projects/Management) that not every Semrush account has. Calls without the corresponding subscription fail with an authorization error. Default to the Standard Analytics API (\`client.report\`) for all SEO/keyword/backlink/competitor analysis. Only reach for \`trends()\` / \`projects()\` when the user explicitly mentions Trends data (traffic analytics, market explorer, etc.) or Projects (site audit, position tracking, etc.) \u2014 and confirm the user has the subscription before building the server logic.
547
+
548
+ \u26A0\uFE0F **Important: \`report()\` row shape.** \`rows\` is \`Record<string, string>[]\` \u2014 each row is an object keyed by the CSV column NAME (matching \`columns\`), NOT a positional array. Access fields with \`row["Url"]\`, \`row["Keyword"]\`, \`row["Search Volume"]\` (column names may contain spaces). Do NOT use \`columns.indexOf("Url")\` and then \`row[index]\` \u2014 that returns \`undefined\` and silently produces empty results when combined with \`?? ""\` or \`Number(...) || 0\`. All values are strings; convert numeric columns with \`Number(row["Position"])\`.
549
+
511
550
  \`\`\`ts
512
551
  import type { Context } from "hono";
513
552
  import { connection } from "@squadbase/vite-server/connectors/semrush";
@@ -520,9 +559,25 @@ export default async function handler(c: Context) {
520
559
  database?: string;
521
560
  }>();
522
561
 
523
- const overview = await semrush.report("domain_overview", { domain, database });
562
+ const result = await semrush.report("domain_organic", {
563
+ domain,
564
+ database,
565
+ display_limit: "100",
566
+ });
567
+
568
+ // \u2705 Correct: access by column name
569
+ const rows = result.rows.map((row) => ({
570
+ keyword: row["Keyword"],
571
+ position: Number(row["Position"]) || 0,
572
+ searchVolume: Number(row["Search Volume"]) || 0,
573
+ url: row["Url"] ?? "",
574
+ }));
575
+
576
+ // \u274C Wrong: do NOT do this \u2014 row[index] is undefined because rows are objects, not arrays
577
+ // const urlIdx = result.columns.indexOf("Url");
578
+ // const url = (row as unknown as string[])[urlIdx];
524
579
 
525
- return c.json({ columns: overview.columns, rows: overview.rows });
580
+ return c.json({ columns: result.columns, rows });
526
581
  }
527
582
  \`\`\`
528
583
 
@@ -534,8 +589,8 @@ export default async function handler(c: Context) {
534
589
 
535
590
  Authentication: API key passed as the \`key\` query parameter on every request (handled automatically).
536
591
 
537
- #### Common Standard Analytics report types
538
- - \`domain_overview\` \u2014 domain summary (organic/paid traffic, keywords, backlinks)
592
+ #### Common Standard Analytics report types (path \`/\`, returns CSV)
593
+ - \`domain_ranks\` \u2014 single-row domain summary (rank, organic/paid keywords, traffic, cost). **There is no \`domain_overview\` type \u2014 use \`domain_ranks\`.**
539
594
  - \`domain_organic\` \u2014 organic keywords for a domain
540
595
  - \`domain_adwords\` \u2014 paid keywords for a domain
541
596
  - \`domain_organic_organic\` / \`domain_adwords_adwords\` \u2014 organic / paid competitors
@@ -544,10 +599,13 @@ Authentication: API key passed as the \`key\` query parameter on every request (
544
599
  - \`phrase_fullsearch\` \u2014 full-text keyword research
545
600
  - \`phrase_questions\` \u2014 question keywords
546
601
  - \`phrase_kdi\` \u2014 keyword difficulty index
602
+ - \`url_organic\` / \`url_adwords\` \u2014 keywords ranking for a specific URL
603
+
604
+ #### Backlinks API report types (path \`/analytics/v1/\`, also returns **CSV** \u2014 not JSON)
605
+ Backlinks endpoints live under \`/analytics/v1/?type=backlinks_*\` and require \`target\` + \`target_type\` (\`root_domain\` | \`domain\` | \`url\`) instead of \`domain\`/\`database\`. They return semicolon-separated CSV, so use \`responseFormat="text"\` from the request tool, and from the SDK use \`client.request("/analytics/v1/", { query: { type: "backlinks_overview", target, target_type: "root_domain" } })\` and parse the CSV yourself \u2014 \`client.trends()\` will throw because it JSON-parses the body.
547
606
  - \`backlinks_overview\` \u2014 backlinks summary
548
607
  - \`backlinks\` \u2014 list of backlinks
549
608
  - \`backlinks_refdomains\` \u2014 referring domains
550
- - \`url_organic\` / \`url_adwords\` \u2014 keywords ranking for a specific URL
551
609
 
552
610
  To check remaining API units (free, does NOT consume units), call the request tool with an absolute URL: \`path: "https://www.semrush.com/users/countapiunits.html"\`, no query params, \`responseFormat: "text"\`. The response body is just a number.
553
611
 
@@ -555,7 +613,7 @@ To check remaining API units (free, does NOT consume units), call the request to
555
613
  - \`type\` \u2014 report type (required for the Standard API)
556
614
  - \`domain\` / \`phrase\` / \`url\` \u2014 entity to query
557
615
  - \`database\` \u2014 regional database (e.g. \`us\`, \`uk\`, \`de\`, \`fr\`, \`jp\`, \`br\`); required for most reports
558
- - \`display_limit\` \u2014 page size (default 10000, max 100000 depending on report)
616
+ - \`display_limit\` \u2014 page size (default 10000, max 100000 depending on report). **Do NOT use the default \u2014 always pass an explicit small value like \`"1000"\`.** Large limits make Semrush pre-charge required units and reject with \`ERROR 132\` even when the balance is non-zero.
559
617
  - \`display_offset\` \u2014 pagination offset
560
618
  - \`display_date\` \u2014 historical date in \`YYYYMM15\` format (always day 15)
561
619
  - \`export_columns\` \u2014 comma-separated columns to return (e.g. \`Ph,Po,Nq,Cp\`)
@@ -566,6 +624,7 @@ To check remaining API units (free, does NOT consume units), call the request to
566
624
  - Each Standard Analytics report consumes API units; check the unit balance via \`https://www.semrush.com/users/countapiunits.html?key=...\` (free) first if you suspect a quota issue
567
625
  - The CSV separator is \`;\` (semicolon), NOT \`,\`. Some cells may contain commas inside them.
568
626
  - An HTTP 200 response with a body starting with \`ERROR\` indicates an API error (auth, parameters, or quota)
627
+ - \`ERROR 132 :: API UNITS BALANCE IS ZERO\` does NOT only mean the balance is literally 0. Semrush also returns it when \`display_limit \xD7 per-row cost\` exceeds the remaining balance. Re-check the balance with \`countapiunits.html\` and lower \`display_limit\` (e.g. to \`1000\` or \`100\`) before assuming the account is empty.
569
628
  - The Trends API requires a separate Trends subscription; calls without it will fail with an authorization error
570
629
  - Date strings in historical endpoints must be the 15th of the month (\`YYYYMM15\`)`,
571
630
  ja: `### \u30C4\u30FC\u30EB
@@ -578,10 +637,19 @@ To check remaining API units (free, does NOT consume units), call the request to
578
637
 
579
638
  SDK\u30E1\u30BD\u30C3\u30C9 (\`connection(connectionId)\` \u3067\u4F5C\u6210\u3057\u305F\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8):
580
639
  - \`client.request(path, init?)\` \u2014 \u8A8D\u8A3C\u4ED8\u304D\u306E\u4F4E\u30EC\u30D9\u30EBfetch\u3002\`init.query\` \u3067\u30AF\u30A8\u30EA\u30D1\u30E9\u30E1\u30FC\u30BF\u3092\u6307\u5B9A\u3002\`key\` \u306F\u81EA\u52D5\u4ED8\u4E0E
581
- - \`client.report(type, query?)\` \u2014 Standard Analytics \u306E\u30EC\u30DD\u30FC\u30C8\u3092\u547C\u3073\u51FA\u3057\u3001CSV\u3092 \`{ columns, rows, raw }\` \u306B\u30D1\u30FC\u30B9
640
+ - \`client.checkUnits()\` \u2014 \u73FE\u5728\u306E API \u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u3092\u6570\u5024\u3067\u8FD4\u3059\u3002**\u7121\u6599\u3067\u3001\u30E6\u30CB\u30C3\u30C8\u3092\u6D88\u8CBB\u3057\u306A\u3044\u3002** \`report()\` \u5B9F\u884C\u524D\u306E\u30D7\u30EA\u30D5\u30E9\u30A4\u30C8\u306B\u4F7F\u3046
641
+ - \`client.report(type, query?)\` \u2014 Standard Analytics \u306E\u30EC\u30DD\u30FC\u30C8\u3092\u547C\u3073\u51FA\u3057\u3001CSV\u3092 \`{ columns: string[], rows: Record<string, string>[], raw: string }\` \u306B\u30D1\u30FC\u30B9\u3002\u6B8B\u91CF\u30BC\u30ED\u306E\u5834\u5408\u306F \`API UNITS BALANCE IS ZERO\` \u3092\u542B\u3080\u30A8\u30E9\u30FC\u3092 throw
582
642
  - \`client.trends(path, query?)\` \u2014 Trends API \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\uFF08\`/analytics/v1/...\`\uFF09\u3092\u547C\u3073\u51FA\u3057 JSON \u3092\u8FD4\u3059
583
643
  - \`client.projects(path, init?)\` \u2014 Projects API \u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\uFF08\`/management/v1/...\`\uFF09\u3092\u547C\u3073\u51FA\u3057 JSON \u3092\u8FD4\u3059
584
644
 
645
+ \u{1F6A8} **\`report()\` \u3092\u547C\u3076\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u3092\u4F5C\u6210\u3059\u308B\u524D\u306E\u5FC5\u9808\u30D7\u30EA\u30D5\u30E9\u30A4\u30C8\u3002** Standard Analytics\uFF08\`domain_ranks\`\u3001\`domain_organic\`\u3001\`phrase_this\` \u306A\u3069\uFF09\u306E \`client.report(...)\` \u3092\u542B\u3080 TypeScript \u30CF\u30F3\u30C9\u30E9\u3092\u4F5C\u6210\u3059\u308B\u524D\u306B\u3001\u5FC5\u305A \`semrush-api-key_request\` \u30C4\u30FC\u30EB\u3092 \`path: "https://www.semrush.com/users/countapiunits.html"\`\u3001\`queryParams\` \u7121\u3057\u3001\`responseFormat: "text"\` \u3067\u547C\u3073\u51FA\u3057\u3066\u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u304C 0 \u3088\u308A\u5927\u304D\u3044\u3053\u3068\u3092\u78BA\u8A8D\u3059\u308B\u3053\u3068\u3002\u6B8B\u91CF\u304C 0 \u306E\u5834\u5408\u306F **\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u3092\u4F5C\u6210\u305B\u305A\u306B\u505C\u6B62\u3057\u3001\u30E6\u30FC\u30B6\u30FC\u306B\u5831\u544A**\u3059\u308B\uFF08Semrush \u306E\u7BA1\u7406\u30B3\u30F3\u30BD\u30FC\u30EB\u304B\u3089 API \u30E6\u30CB\u30C3\u30C8\u3092\u88DC\u5145\u3059\u308B\u5FC5\u8981\u304C\u3042\u308B\uFF09\u3002\`testFetchServerLogic\` \u306F\u30CF\u30F3\u30C9\u30E9\u3092\u5B9F\u884C\u3057\u3066\u30E6\u30CB\u30C3\u30C8\u3092\u6D88\u8CBB\u3059\u308B\u305F\u3081\u3001\u6B8B\u91CF 0 \u306E\u307E\u307E\u4F5C\u6210\u3059\u308B\u3068\u5168\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u304C \`ERROR 132 :: API UNITS BALANCE IS ZERO\` \u3067\u5931\u6557\u3059\u308B\u3002
646
+
647
+ \u{1F6A8} **\`display_limit\` \u306F\u5FC5\u305A\u660E\u793A\u7684\u306B\u5C0F\u3055\u3044\u5024\u3092\u6307\u5B9A\u3057\u3001\u30C7\u30D5\u30A9\u30EB\u30C8\u306B\u983C\u3089\u306A\u3044\u3053\u3068\u3002** Semrush \u306F\u4E8B\u524D\u306B\u300C\u5FC5\u8981\u30E6\u30CB\u30C3\u30C8 = display_limit \xD7 \u884C\u3042\u305F\u308A\u5358\u4FA1\u300D\u3092\u8A08\u7B97\u3057\u3001\u305D\u306E**\u898B\u7A4D\u984D**\u304C\u6B8B\u91CF\u3092\u8D85\u3048\u308B\u3068\u30EA\u30AF\u30A8\u30B9\u30C8\u3092 \`ERROR 132 :: API UNITS BALANCE IS ZERO\` \u3067\u62D2\u5426\u3059\u308B\u3002**\u5B9F\u6B8B\u91CF\u304C\u30BC\u30ED\u3067\u306A\u304F\u3066\u3082**\u8D77\u3053\u308B\uFF08\u4F8B\uFF1A\u6B8B\u91CF\u7D04 40,950 \u3067\u3082\u3001\u30B3\u30B9\u30C8\u306E\u9AD8\u3044\u30EC\u30DD\u30FC\u30C8\u3067 \`display_limit=10000\` \u3060\u3068\u5931\u6557\u3059\u308B\uFF09\u3002\u539F\u5247\u3068\u3057\u3066 \`display_limit: "1000"\`\uFF08\u30C0\u30C3\u30B7\u30E5\u30DC\u30FC\u30C9\u7528\u9014\u306A\u3089 \`"100"\` \u3067\u3082\u5341\u5206\uFF09\u3092\u6307\u5B9A\u3057\u3001\u30E6\u30FC\u30B6\u30FC\u304C\u660E\u793A\u7684\u306B\u5927\u304D\u306A\u30B5\u30F3\u30D7\u30EB\u3092\u8981\u6C42\u3057\u3001\u304B\u3064 \`checkUnits()\` \u3067\u6B8B\u91CF\u306B\u5341\u5206\u306A\u4F59\u88D5\u304C\u3042\u308B\u3053\u3068\u3092\u78BA\u8A8D\u3067\u304D\u305F\u5834\u5408\u306E\u307F\u5897\u3084\u3059\u3002\u5168\u884C\u304C\u5FC5\u8981\u306A\u5834\u5408\u306F \`display_limit\` \u3092\u4E0A\u3052\u308B\u306E\u3067\u306F\u306A\u304F\u3001\`display_offset\` \u3067 1000 \u4EF6\u523B\u307F\u306E\u30DA\u30FC\u30B8\u30CD\u30FC\u30B7\u30E7\u30F3\u3092\u5B9F\u88C5\u3059\u308B\u3053\u3068\u3002\`ERROR 132\` \u306F\u300C\u6B8B\u91CF\u304C\u6587\u5B57\u901A\u308A\u30BC\u30ED\u300D\u3088\u308A\u5148\u306B\u300C\u73FE\u5728\u306E\u6B8B\u91CF\u306B\u5BFE\u3057\u3066\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u5927\u304D\u3059\u304E\u308B\u300D\u3092\u7591\u3044\u3001\u30E6\u30CB\u30C3\u30C8\u3092\u518D\u78BA\u8A8D\u3057\u3066\u304B\u3089\u5224\u65AD\u3059\u308B\u3053\u3068\u3002
648
+
649
+ \u{1F6AB} **Trends API\uFF08\`client.trends\`\uFF09\u3068 Projects API\uFF08\`client.projects\`\uFF09\u306F\u3001\u30E6\u30FC\u30B6\u30FC\u304B\u3089\u660E\u793A\u7684\u306B\u6307\u793A\u3055\u308C\u306A\u3044\u9650\u308A\u4F7F\u7528\u3057\u306A\u3044\u3053\u3068\u3002** \u3053\u308C\u3089\u306E API \u306F\u5225\u9014\u6709\u6599\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\uFF08Trends / Projects\uFF08Management\uFF09\uFF09\u304C\u5FC5\u8981\u3067\u3001\u3059\u3079\u3066\u306E Semrush \u30A2\u30AB\u30A6\u30F3\u30C8\u304C\u5951\u7D04\u3057\u3066\u3044\u308B\u308F\u3051\u3067\u306F\u306A\u3044\u3002\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u304C\u306A\u3044\u72B6\u614B\u3067\u547C\u3073\u51FA\u3059\u3068\u8A8D\u53EF\u30A8\u30E9\u30FC\u3067\u5931\u6557\u3059\u308B\u3002SEO\u30FB\u30AD\u30FC\u30EF\u30FC\u30C9\u30FB\u88AB\u30EA\u30F3\u30AF\u30FB\u7AF6\u5408\u5206\u6790\u306F\u539F\u5247 Standard Analytics API\uFF08\`client.report\`\uFF09\u3067\u884C\u3046\u3002\`trends()\` / \`projects()\` \u3092\u4F7F\u3046\u306E\u306F\u3001\u30E6\u30FC\u30B6\u30FC\u304C Trends \u30C7\u30FC\u30BF\uFF08\u30C8\u30E9\u30D5\u30A3\u30C3\u30AF\u30A2\u30CA\u30EA\u30C6\u30A3\u30AF\u30B9\u3001\u30DE\u30FC\u30B1\u30C3\u30C8\u30A8\u30AF\u30B9\u30D7\u30ED\u30FC\u30E9\u30FC\u7B49\uFF09\u307E\u305F\u306F Projects \u6A5F\u80FD\uFF08\u30B5\u30A4\u30C8\u76E3\u67FB\u3001\u30DD\u30B8\u30B7\u30E7\u30F3\u30C8\u30E9\u30C3\u30AD\u30F3\u30B0\u7B49\uFF09\u3092**\u660E\u793A\u7684\u306B\u6C42\u3081\u305F\u5834\u5408\u306E\u307F**\u3067\u3001\u305D\u306E\u969B\u3082\u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u306E\u6709\u7121\u3092\u30E6\u30FC\u30B6\u30FC\u306B\u78BA\u8A8D\u3057\u3066\u304B\u3089\u30B5\u30FC\u30D0\u30FC\u30ED\u30B8\u30C3\u30AF\u3092\u4F5C\u6210\u3059\u308B\u3053\u3068\u3002
650
+
651
+ \u26A0\uFE0F **\u91CD\u8981: \`report()\` \u306E\u884C\uFF08row\uFF09\u306E\u5F62\u72B6\u306B\u3064\u3044\u3066\u3002** \`rows\` \u306F \`Record<string, string>[]\` \u3067\u3059 \u2014 \u5404\u884C\u306F CSV \u306E\u30AB\u30E9\u30E0\u300C\u540D\u300D\uFF08\`columns\` \u3068\u4E00\u81F4\uFF09\u3092\u30AD\u30FC\u3068\u3057\u305F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308A\u3001\u4F4D\u7F6E\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306E\u914D\u5217\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u30D5\u30A3\u30FC\u30EB\u30C9\u3078\u306E\u30A2\u30AF\u30BB\u30B9\u306F\u5FC5\u305A \`row["Url"]\`\u3001\`row["Keyword"]\`\u3001\`row["Search Volume"]\`\uFF08\u30AB\u30E9\u30E0\u540D\u306B\u30B9\u30DA\u30FC\u30B9\u3092\u542B\u3080\u3053\u3068\u3042\u308A\uFF09\u306E\u3088\u3046\u306B\u30AB\u30E9\u30E0\u540D\u3067\u884C\u3063\u3066\u304F\u3060\u3055\u3044\u3002\`columns.indexOf("Url")\` \u3067\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u3092\u53D6\u5F97\u3057\u3066\u304B\u3089 \`row[index]\` \u3067\u30A2\u30AF\u30BB\u30B9\u3057\u3066\u306F\u3044\u3051\u307E\u305B\u3093 \u2014 \u305D\u308C\u306F \`undefined\` \u3092\u8FD4\u3057\u3001\`?? ""\` \u3084 \`Number(...) || 0\` \u3068\u7D44\u307F\u5408\u308F\u3055\u308B\u3053\u3068\u3067\u7D50\u679C\u304C\u7A7A\u306B\u306A\u308B\u7121\u97F3\u306E\u5931\u6557\u3092\u751F\u307F\u307E\u3059\u3002\u5168\u3066\u306E\u5024\u306F\u6587\u5B57\u5217\u306A\u306E\u3067\u3001\u6570\u5024\u30AB\u30E9\u30E0\u306F \`Number(row["Position"])\` \u3067\u660E\u793A\u5909\u63DB\u3057\u3066\u304F\u3060\u3055\u3044\u3002
652
+
585
653
  \`\`\`ts
586
654
  import type { Context } from "hono";
587
655
  import { connection } from "@squadbase/vite-server/connectors/semrush";
@@ -594,9 +662,25 @@ export default async function handler(c: Context) {
594
662
  database?: string;
595
663
  }>();
596
664
 
597
- const overview = await semrush.report("domain_overview", { domain, database });
665
+ const result = await semrush.report("domain_organic", {
666
+ domain,
667
+ database,
668
+ display_limit: "100",
669
+ });
670
+
671
+ // \u2705 \u6B63: \u30AB\u30E9\u30E0\u540D\u3067\u30A2\u30AF\u30BB\u30B9\u3059\u308B
672
+ const rows = result.rows.map((row) => ({
673
+ keyword: row["Keyword"],
674
+ position: Number(row["Position"]) || 0,
675
+ searchVolume: Number(row["Search Volume"]) || 0,
676
+ url: row["Url"] ?? "",
677
+ }));
678
+
679
+ // \u274C \u8AA4: \u884C\u306F\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308A\u914D\u5217\u3067\u306F\u306A\u3044\u305F\u3081 row[index] \u306F undefined \u306B\u306A\u308B
680
+ // const urlIdx = result.columns.indexOf("Url");
681
+ // const url = (row as unknown as string[])[urlIdx];
598
682
 
599
- return c.json({ columns: overview.columns, rows: overview.rows });
683
+ return c.json({ columns: result.columns, rows });
600
684
  }
601
685
  \`\`\`
602
686
 
@@ -608,8 +692,8 @@ export default async function handler(c: Context) {
608
692
 
609
693
  \u8A8D\u8A3C: API\u30AD\u30FC\u3092\u3059\u3079\u3066\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u306B \`key\` \u30AF\u30A8\u30EA\u30D1\u30E9\u30E1\u30FC\u30BF\u3068\u3057\u3066\u4ED8\u4E0E\uFF08\u81EA\u52D5\uFF09\u3002
610
694
 
611
- #### \u4E3B\u8981\u306A Standard Analytics \u30EC\u30DD\u30FC\u30C8\u30BF\u30A4\u30D7
612
- - \`domain_overview\` \u2014 \u30C9\u30E1\u30A4\u30F3\u306E\u30B5\u30DE\u30EA\u30FC\uFF08\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF/\u6709\u6599\u30C8\u30E9\u30D5\u30A3\u30C3\u30AF\u3001\u30AD\u30FC\u30EF\u30FC\u30C9\u3001\u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\uFF09
695
+ #### \u4E3B\u8981\u306A Standard Analytics \u30EC\u30DD\u30FC\u30C8\u30BF\u30A4\u30D7\uFF08path \`/\`\u3001CSV\u3092\u8FD4\u3059\uFF09
696
+ - \`domain_ranks\` \u2014 \u30C9\u30E1\u30A4\u30F3\u306E\u30B5\u30DE\u30EA\u30FC\uFF08rank\u3001\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF/\u6709\u6599\u30AD\u30FC\u30EF\u30FC\u30C9\u6570\u3001\u30C8\u30E9\u30D5\u30A3\u30C3\u30AF\u3001\u30B3\u30B9\u30C8\u306E1\u884CCSV\uFF09\u3002**\`domain_overview\` \u3068\u3044\u3046\u30BF\u30A4\u30D7\u306F\u5B58\u5728\u3057\u306A\u3044\u3002\`domain_ranks\` \u3092\u4F7F\u3046\u3053\u3068\u3002**
613
697
  - \`domain_organic\` \u2014 \u30C9\u30E1\u30A4\u30F3\u306E\u30AA\u30FC\u30AC\u30CB\u30C3\u30AF\u30AD\u30FC\u30EF\u30FC\u30C9
614
698
  - \`domain_adwords\` \u2014 \u30C9\u30E1\u30A4\u30F3\u306E\u6709\u6599\u30AD\u30FC\u30EF\u30FC\u30C9
615
699
  - \`domain_organic_organic\` / \`domain_adwords_adwords\` \u2014 \u30AA\u30FC\u30AC\u30CB\u30C3\u30AF\uFF0F\u6709\u6599\u306E\u7AF6\u5408
@@ -618,10 +702,13 @@ export default async function handler(c: Context) {
618
702
  - \`phrase_fullsearch\` \u2014 \u30D5\u30EB\u30C6\u30AD\u30B9\u30C8\u30AD\u30FC\u30EF\u30FC\u30C9\u30EA\u30B5\u30FC\u30C1
619
703
  - \`phrase_questions\` \u2014 \u8CEA\u554F\u5F62\u5F0F\u30AD\u30FC\u30EF\u30FC\u30C9
620
704
  - \`phrase_kdi\` \u2014 \u30AD\u30FC\u30EF\u30FC\u30C9\u96E3\u6613\u5EA6\uFF08KDI\uFF09
705
+ - \`url_organic\` / \`url_adwords\` \u2014 \u7279\u5B9AURL\u3067\u30E9\u30F3\u30AF\u30A4\u30F3\u3057\u3066\u3044\u308B\u30AD\u30FC\u30EF\u30FC\u30C9
706
+
707
+ #### Backlinks API \u30EC\u30DD\u30FC\u30C8\u30BF\u30A4\u30D7\uFF08path \`/analytics/v1/\`\u3001\u3053\u3061\u3089\u3082 **CSV** \u3092\u8FD4\u3059\u3002JSON \u3067\u306F\u306A\u3044\uFF09
708
+ Backlinks \u7CFB\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306F \`/analytics/v1/?type=backlinks_*\` \u914D\u4E0B\u306B\u3042\u308A\u3001\`domain\`/\`database\` \u3067\u306F\u306A\u304F \`target\` + \`target_type\`\uFF08\`root_domain\` | \`domain\` | \`url\`\uFF09\u3092\u8981\u6C42\u3059\u308B\u3002\u30EC\u30B9\u30DD\u30F3\u30B9\u306F\u30BB\u30DF\u30B3\u30ED\u30F3\u533A\u5207\u308ACSV\u306A\u306E\u3067\u3001request \u30C4\u30FC\u30EB\u3067\u306F \`responseFormat="text"\` \u3092\u4F7F\u3044\u3001SDK \u3067\u306F \`client.request("/analytics/v1/", { query: { type: "backlinks_overview", target, target_type: "root_domain" } })\` \u3092\u547C\u3093\u3067 CSV \u3092\u81EA\u524D\u3067\u30D1\u30FC\u30B9\u3059\u308B\u3053\u3068\u3002\`client.trends()\` \u306F JSON.parse \u3059\u308B\u305F\u3081 backlinks \u7CFB\u3067\u4F7F\u3046\u3068 throw \u3059\u308B\u3002
621
709
  - \`backlinks_overview\` \u2014 \u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u6982\u8981
622
710
  - \`backlinks\` \u2014 \u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u4E00\u89A7
623
711
  - \`backlinks_refdomains\` \u2014 \u53C2\u7167\u30C9\u30E1\u30A4\u30F3
624
- - \`url_organic\` / \`url_adwords\` \u2014 \u7279\u5B9AURL\u3067\u30E9\u30F3\u30AF\u30A4\u30F3\u3057\u3066\u3044\u308B\u30AD\u30FC\u30EF\u30FC\u30C9
625
712
 
626
713
  API \u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u306E\u78BA\u8A8D\uFF08\u7121\u6599\u3001\u30E6\u30CB\u30C3\u30C8\u3092\u6D88\u8CBB\u3057\u306A\u3044\uFF09\u306F request \u30C4\u30FC\u30EB\u306B\u7D76\u5BFEURL\u3092\u6E21\u3059: \`path: "https://www.semrush.com/users/countapiunits.html"\`\u3001queryParams \u306A\u3057\u3001\`responseFormat: "text"\`\u3002\u30EC\u30B9\u30DD\u30F3\u30B9\u672C\u6587\u306F\u6570\u5024\u306E\u307F\u3002
627
714
 
@@ -629,7 +716,7 @@ API \u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u306E\u78BA\u8A8D\uFF08\u7121\u6599\u30
629
716
  - \`type\` \u2014 \u30EC\u30DD\u30FC\u30C8\u7A2E\u5225\uFF08Standard API \u3067\u306F\u5FC5\u9808\uFF09
630
717
  - \`domain\` / \`phrase\` / \`url\` \u2014 \u30AF\u30A8\u30EA\u5BFE\u8C61\u306E\u30A8\u30F3\u30C6\u30A3\u30C6\u30A3
631
718
  - \`database\` \u2014 \u5730\u57DF\u5225\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\uFF08\`us\`, \`uk\`, \`de\`, \`fr\`, \`jp\`, \`br\` \u306A\u3069\uFF09\u3002\u591A\u304F\u306E\u30EC\u30DD\u30FC\u30C8\u3067\u5FC5\u9808
632
- - \`display_limit\` \u2014 \u30DA\u30FC\u30B8\u30B5\u30A4\u30BA\uFF08\u30C7\u30D5\u30A9\u30EB\u30C810000\u3001\u30EC\u30DD\u30FC\u30C8\u306B\u3088\u3063\u3066\u306F\u6700\u5927100000\uFF09
719
+ - \`display_limit\` \u2014 \u30DA\u30FC\u30B8\u30B5\u30A4\u30BA\uFF08\u30C7\u30D5\u30A9\u30EB\u30C810000\u3001\u30EC\u30DD\u30FC\u30C8\u306B\u3088\u3063\u3066\u306F\u6700\u5927100000\uFF09\u3002**\u30C7\u30D5\u30A9\u30EB\u30C8\u3092\u4F7F\u308F\u305A\u3001\u5FC5\u305A \`"1000"\` \u7A0B\u5EA6\u306E\u5C0F\u3055\u3044\u5024\u3092\u660E\u793A\u7684\u306B\u6307\u5B9A\u3059\u308B\u3053\u3068\u3002** \u5927\u304D\u3044\u5024\u3060\u3068 Semrush \u304C\u5FC5\u8981\u30E6\u30CB\u30C3\u30C8\u3092\u4E8B\u524D\u8A08\u7B97\u3057\u3001\u6B8B\u91CF\u304C\u3042\u3063\u3066\u3082 \`ERROR 132\` \u3067\u62D2\u5426\u3059\u308B\u3002
633
720
  - \`display_offset\` \u2014 \u30DA\u30FC\u30B8\u30CD\u30FC\u30B7\u30E7\u30F3\u30AA\u30D5\u30BB\u30C3\u30C8
634
721
  - \`display_date\` \u2014 \u5C65\u6B74\u306E\u65E5\u4ED8\u3002\`YYYYMM15\` \u5F62\u5F0F\uFF08\u5FC5\u305A\u6708\u306E15\u65E5\uFF09
635
722
  - \`export_columns\` \u2014 \u8FD4\u5374\u30AB\u30E9\u30E0\u3092\u30AB\u30F3\u30DE\u533A\u5207\u308A\u3067\u6307\u5B9A\uFF08\u4F8B: \`Ph,Po,Nq,Cp\`\uFF09
@@ -640,6 +727,7 @@ API \u30E6\u30CB\u30C3\u30C8\u6B8B\u91CF\u306E\u78BA\u8A8D\uFF08\u7121\u6599\u30
640
727
  - Standard Analytics \u306E\u5404\u30EC\u30DD\u30FC\u30C8\u306F API \u30E6\u30CB\u30C3\u30C8\u3092\u6D88\u8CBB\u3059\u308B\u3002\u30AF\u30A9\u30FC\u30BF\u304C\u7591\u308F\u3057\u3044\u5834\u5408\u306F \`https://www.semrush.com/users/countapiunits.html?key=...\` \u3067\u6B8B\u91CF\u3092\u5148\u306B\u78BA\u8A8D\u3059\u308B\uFF08\u7121\u6599\uFF09
641
728
  - CSV \u306E\u30BB\u30D1\u30EC\u30FC\u30BF\u306F \`;\`\uFF08\u30BB\u30DF\u30B3\u30ED\u30F3\uFF09\u3067\u3042\u308A \`,\` \u3067\u306F\u306A\u3044\u3002\u30BB\u30EB\u5185\u306B\u30AB\u30F3\u30DE\u304C\u542B\u307E\u308C\u308B\u3053\u3068\u304C\u3042\u308B
642
729
  - HTTP 200 \u3067\u3082\u672C\u6587\u304C \`ERROR\` \u3067\u59CB\u307E\u308B\u5834\u5408\u306F API\u30A8\u30E9\u30FC\uFF08\u8A8D\u8A3C\u3001\u30D1\u30E9\u30E1\u30FC\u30BF\u3001\u30AF\u30A9\u30FC\u30BF\uFF09
730
+ - \`ERROR 132 :: API UNITS BALANCE IS ZERO\` \u306F\u300C\u6B8B\u91CF\u304C\u6587\u5B57\u901A\u308A 0\u300D\u3060\u3051\u3092\u610F\u5473\u3059\u308B\u308F\u3051\u3067\u306F\u306A\u3044\u3002Semrush \u306F \`display_limit \xD7 \u884C\u3042\u305F\u308A\u5358\u4FA1\` \u304C\u6B8B\u91CF\u3092\u8D85\u3048\u308B\u5834\u5408\u3082\u540C\u3058\u30A8\u30E9\u30FC\u3092\u8FD4\u3059\u3002\`countapiunits.html\` \u3067\u6B8B\u91CF\u3092\u518D\u78BA\u8A8D\u3057\u3001\`display_limit\` \u3092 \`1000\` \u3084 \`100\` \u307E\u3067\u4E0B\u3052\u3066\u304B\u3089\u300C\u6B8B\u91CF\u5207\u308C\u300D\u3068\u5224\u65AD\u3059\u308B\u3053\u3068\u3002
643
731
  - Trends API \u306F\u5225\u9014 Trends \u30B5\u30D6\u30B9\u30AF\u30EA\u30D7\u30B7\u30E7\u30F3\u304C\u5FC5\u8981\u3002\u672A\u5951\u7D04\u3060\u3068\u8A8D\u53EF\u30A8\u30E9\u30FC\u306B\u306A\u308B
644
732
  - \u5C65\u6B74\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306E\u65E5\u4ED8\u306F\u5FC5\u305A\u6708\u306E15\u65E5\uFF08\`YYYYMM15\`\uFF09`
645
733
  },