[{"data":1,"prerenderedAt":948},["ShallowReactive",2],{"content-/chrome-extension-sheets-api":3,"all-pages-for-dir":946,"og-image-/chrome-extension-sheets-api":947},{"id":4,"title":5,"body":6,"category":928,"description":929,"extension":930,"meta":931,"navigation":324,"ogImage":932,"path":933,"project_name":934,"published":935,"publishedAt":936,"seo":937,"stem":938,"tags":939,"todo":944,"unpublished":935,"updatedAt":932,"__hash__":945},"pages/2026-03/2026-03-22/chrome-extension-sheets-api.md","Chrome拡張機能にGoogle Sheets API書き込みを追加する - OAuth2認証からdiff追記まで",{"type":7,"value":8,"toc":912},"minimark",[9,14,18,21,26,38,43,54,62,169,176,178,182,189,230,240,243,245,249,252,263,362,373,380,382,386,389,392,417,553,556,558,562,565,568,570,573,580,583,586,590,593,607,891,894,896,899,902,905,908],[10,11,13],"h1",{"id":12},"chrome拡張機能にgoogle-sheets-api書き込みを追加する","Chrome拡張機能にGoogle Sheets API書き込みを追加する",[15,16,17],"p",{},"会計ソフトの明細を自動取得するChrome拡張を以前から使っていたが、出力はCSVダウンロードだけだった。取得したデータをそのままGoogle Sheetsに書き込めれば、ダウンロード→手動インポートの手間が消える。OAuth2認証の設定で2回つまずき、API URLで1回つまずき、差分追記のロジックを組んで、最後にページ遷移の待機を直した。半日の作業ログ。",[19,20],"hr",{},[22,23,25],"h2",{"id":24},"oauth2認証-クライアントidのタイプを間違えた","OAuth2認証: クライアントIDのタイプを間違えた",[15,27,28,29,33,34,37],{},"Chrome拡張からGoogle APIを叩くには、",[30,31,32],"code",{},"chrome.identity.getAuthToken()"," でOAuth2トークンを取得する。manifest.jsonに ",[30,35,36],{},"oauth2"," セクションを追加して、Google Cloud ConsoleのクライアントIDを設定するだけ――のはずだった。",[39,40,42],"h3",{"id":41},"最初のエラー-bad-client-id","最初のエラー: bad client id",[15,44,45,46,49,50,53],{},"既存のデスクトップアプリ用クライアントIDをそのまま使ったら、",[30,47,48],{},"getAuthToken()"," が ",[30,51,52],{},"bad client id"," を返した。",[15,55,56,57,61],{},"原因はクライアントIDの",[58,59,60],"strong",{},"タイプ","。Chrome拡張向けには「Chrome アプリ」タイプで新規作成する必要がある。Google Cloud Consoleの認証情報画面で「OAuth 2.0 クライアント ID の作成」→ アプリケーションの種類で「Chrome アプリ」を選び、拡張機能のIDを登録する。",[63,64,69],"pre",{"className":65,"code":66,"language":67,"meta":68,"style":68},"language-json shiki shiki-themes vitesse-light vitesse-light","// manifest.json\n{\n  \"oauth2\": {\n    \"client_id\": \"xxxx.apps.googleusercontent.com\",\n    \"scopes\": [\"https://www.googleapis.com/auth/spreadsheets\"]\n  }\n}\n","json","",[30,70,71,80,87,106,132,157,163],{"__ignoreMap":68},[72,73,76],"span",{"class":74,"line":75},"line",1,[72,77,79],{"class":78},"sxvE3","// manifest.json\n",[72,81,83],{"class":74,"line":82},2,[72,84,86],{"class":85},"shFtX","{\n",[72,88,90,94,97,100,103],{"class":74,"line":89},3,[72,91,93],{"class":92},"sqvqQ","  \"",[72,95,36],{"class":96},"sz8Xr",[72,98,99],{"class":92},"\"",[72,101,102],{"class":85},":",[72,104,105],{"class":85}," {\n",[72,107,109,112,115,117,119,123,127,129],{"class":74,"line":108},4,[72,110,111],{"class":92},"    \"",[72,113,114],{"class":96},"client_id",[72,116,99],{"class":92},[72,118,102],{"class":85},[72,120,122],{"class":121},"sMJiu"," \"",[72,124,126],{"class":125},"sdGka","xxxx.apps.googleusercontent.com",[72,128,99],{"class":121},[72,130,131],{"class":85},",\n",[72,133,135,137,140,142,144,147,149,152,154],{"class":74,"line":134},5,[72,136,111],{"class":92},[72,138,139],{"class":96},"scopes",[72,141,99],{"class":92},[72,143,102],{"class":85},[72,145,146],{"class":85}," [",[72,148,99],{"class":121},[72,150,151],{"class":125},"https://www.googleapis.com/auth/spreadsheets",[72,153,99],{"class":121},[72,155,156],{"class":85},"]\n",[72,158,160],{"class":74,"line":159},6,[72,161,162],{"class":85},"  }\n",[72,164,166],{"class":74,"line":165},7,[72,167,168],{"class":85},"}\n",[15,170,171,172,175],{},"「Chrome アプリ」タイプを選ぶと、クライアントIDがChrome拡張のIDと紐づく。デスクトップ用やWeb用のクライアントIDではこの紐づけが存在しないため、",[30,173,174],{},"chrome.identity"," がIDを弾く。",[19,177],{},[22,179,181],{"id":180},"host_permissions漏れ-codexレビューで発覚","host_permissions漏れ: Codexレビューで発覚",[15,183,184,185,188],{},"manifest.jsonにoauth2セクションを追加した段階で、Codexにレビューを投げた。返ってきた指摘の1つが ",[30,186,187],{},"host_permissions"," の漏れだった。",[63,190,192],{"className":65,"code":191,"language":67,"meta":68,"style":68},"{\n  \"host_permissions\": [\n    \"https://www.googleapis.com/*\"\n  ]\n}\n",[30,193,194,198,211,221,226],{"__ignoreMap":68},[72,195,196],{"class":74,"line":75},[72,197,86],{"class":85},[72,199,200,202,204,206,208],{"class":74,"line":82},[72,201,93],{"class":92},[72,203,187],{"class":96},[72,205,99],{"class":92},[72,207,102],{"class":85},[72,209,210],{"class":85}," [\n",[72,212,213,215,218],{"class":74,"line":89},[72,214,111],{"class":121},[72,216,217],{"class":125},"https://www.googleapis.com/*",[72,219,220],{"class":121},"\"\n",[72,222,223],{"class":74,"line":108},[72,224,225],{"class":85},"  ]\n",[72,227,228],{"class":74,"line":134},[72,229,168],{"class":85},[15,231,232,233,236,237,239],{},"Chrome拡張のservice workerから外部APIへ ",[30,234,235],{},"fetch"," するには、",[30,238,187],{}," にそのドメインを明示的に追加しなければならない。content scriptからのリクエストとは権限モデルが異なる。これが漏れるとfetchがブロックされて無言で失敗する。",[15,241,242],{},"Codexの指摘がなければ、実行時に「なぜか通信できない」と頭を抱えていたはず。レビューを挟む価値を実感した瞬間だった。",[19,244],{},[22,246,248],{"id":247},"api-url-googleapiscom-の罠","API URL: googleapis.com の罠",[15,250,251],{},"認証が通り、host_permissionsも設定した。いざシートに書き込もうとして、今度は404が返ってきた。",[15,253,254,255,258,259,262],{},"原因はベースURL。最初に書いたコードでは ",[30,256,257],{},"https://www.googleapis.com/v4/spreadsheets/..."," を叩いていたが、Sheets API v4の正しいベースURLは ",[30,260,261],{},"https://sheets.googleapis.com/v4/spreadsheets/..."," だった。",[63,264,268],{"className":265,"code":266,"language":267,"meta":68,"style":68},"language-javascript shiki shiki-themes vitesse-light vitesse-light","// NG: 404\nconst url = `https://www.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}`;\n\n// OK\nconst url = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${range}`;\n","javascript",[30,269,270,275,320,326,331],{"__ignoreMap":68},[72,271,272],{"class":74,"line":75},[72,273,274],{"class":78},"// NG: 404\n",[72,276,277,281,285,288,291,294,298,301,304,307,309,312,314,317],{"class":74,"line":82},[72,278,280],{"class":279},"stQ0i","const",[72,282,284],{"class":283},"s4oTP"," url",[72,286,287],{"class":85}," =",[72,289,290],{"class":121}," `",[72,292,293],{"class":125},"https://www.googleapis.com/v4/spreadsheets/",[72,295,297],{"class":296},"sHkkW","${",[72,299,300],{"class":125},"sheetId",[72,302,303],{"class":296},"}",[72,305,306],{"class":125},"/values/",[72,308,297],{"class":296},[72,310,311],{"class":125},"range",[72,313,303],{"class":296},[72,315,316],{"class":121},"`",[72,318,319],{"class":85},";\n",[72,321,322],{"class":74,"line":89},[72,323,325],{"emptyLinePlaceholder":324},true,"\n",[72,327,328],{"class":74,"line":108},[72,329,330],{"class":78},"// OK\n",[72,332,333,335,337,339,341,344,346,348,350,352,354,356,358,360],{"class":74,"line":134},[72,334,280],{"class":279},[72,336,284],{"class":283},[72,338,287],{"class":85},[72,340,290],{"class":121},[72,342,343],{"class":125},"https://sheets.googleapis.com/v4/spreadsheets/",[72,345,297],{"class":296},[72,347,300],{"class":125},[72,349,303],{"class":296},[72,351,306],{"class":125},[72,353,297],{"class":296},[72,355,311],{"class":125},[72,357,303],{"class":296},[72,359,316],{"class":121},[72,361,319],{"class":85},[15,363,364,365,368,369,372],{},"Google APIはサービスごとにサブドメインが異なる。",[30,366,367],{},"www.googleapis.com"," はDiscovery APIなど一部のエンドポイントで使われるが、Sheets APIは ",[30,370,371],{},"sheets.googleapis.com"," に住んでいる。公式ドキュメントのサンプルコードをコピーすれば間違えないが、記憶で書くとこういう罠を踏む。",[15,374,375,376,379],{},"host_permissionsにも ",[30,377,378],{},"https://sheets.googleapis.com/*"," を追加して解決した。",[19,381],{},[22,383,385],{"id":384},"差分追記-全上書きではなくdiffで追記する","差分追記: 全上書きではなくdiffで追記する",[15,387,388],{},"APIが通ったので、次は書き込みロジック。最初は「毎回全データを上書き」で実装しようとしたが、Google Sheetsのセル編集履歴が全て吹き飛ぶことに気づいて方針を変えた。手動でメモを追記していたセルがあったため、全上書きは使えない。",[39,390,391],{"id":391},"実装方針",[393,394,395,403,410],"ol",{},[396,397,398,399,402],"li",{},"既存データを ",[30,400,401],{},"spreadsheets.values.get"," で取得",[396,404,405,406,409],{},"新規データの各行について、既存行と",[58,407,408],{},"全列のJSON.stringify一致","で重複判定",[396,411,412,413,416],{},"既存に存在しない行だけを ",[30,414,415],{},"spreadsheets.values.append"," で追記",[63,418,420],{"className":265,"code":419,"language":267,"meta":68,"style":68},"const isDuplicate = (newRow, existingRows) =>\n  existingRows.some(existing =>\n    JSON.stringify(existing) === JSON.stringify(newRow)\n  );\n\nconst rowsToAppend = newRows.filter(row => !isDuplicate(row, existingRows));\n",[30,421,422,450,469,502,507,511],{"__ignoreMap":68},[72,423,424,426,430,432,435,438,441,444,447],{"class":74,"line":75},[72,425,280],{"class":279},[72,427,429],{"class":428},"senZ8"," isDuplicate",[72,431,287],{"class":85},[72,433,434],{"class":85}," (",[72,436,437],{"class":283},"newRow",[72,439,440],{"class":85},",",[72,442,443],{"class":283}," existingRows",[72,445,446],{"class":85},")",[72,448,449],{"class":85}," =>\n",[72,451,452,455,458,461,464,467],{"class":74,"line":82},[72,453,454],{"class":283},"  existingRows",[72,456,457],{"class":85},".",[72,459,460],{"class":428},"some",[72,462,463],{"class":85},"(",[72,465,466],{"class":283},"existing",[72,468,449],{"class":85},[72,470,471,474,476,479,481,483,485,488,491,493,495,497,499],{"class":74,"line":89},[72,472,473],{"class":283},"    JSON",[72,475,457],{"class":85},[72,477,478],{"class":428},"stringify",[72,480,463],{"class":85},[72,482,466],{"class":283},[72,484,446],{"class":85},[72,486,487],{"class":279}," ===",[72,489,490],{"class":283}," JSON",[72,492,457],{"class":85},[72,494,478],{"class":428},[72,496,463],{"class":85},[72,498,437],{"class":283},[72,500,501],{"class":85},")\n",[72,503,504],{"class":74,"line":108},[72,505,506],{"class":85},"  );\n",[72,508,509],{"class":74,"line":134},[72,510,325],{"emptyLinePlaceholder":324},[72,512,513,515,518,520,523,525,528,530,533,536,539,542,544,546,548,550],{"class":74,"line":159},[72,514,280],{"class":279},[72,516,517],{"class":283}," rowsToAppend",[72,519,287],{"class":85},[72,521,522],{"class":283}," newRows",[72,524,457],{"class":85},[72,526,527],{"class":428},"filter",[72,529,463],{"class":85},[72,531,532],{"class":283},"row",[72,534,535],{"class":85}," =>",[72,537,538],{"class":279}," !",[72,540,541],{"class":428},"isDuplicate",[72,543,463],{"class":85},[72,545,532],{"class":283},[72,547,440],{"class":85},[72,549,443],{"class":283},[72,551,552],{"class":85},"));\n",[15,554,555],{},"JSON.stringifyでの比較は雑に見えるが、会計明細のデータは日付・金額・摘要が全て一致すれば同一行とみなして問題ない。部分一致や曖昧マッチングを入れると逆に誤判定が増える。「全列一致なら重複」というルールで2週間運用して、誤判定は一度も起きていない。",[19,557],{},[22,559,561],{"id":560},"uiの変更-チェックボックスからラジオボタンへ","UIの変更: チェックボックスからラジオボタンへ",[15,563,564],{},"元々のUIでは「CSVダウンロード」と「Sheets書き込み」を独立したチェックボックスで並べていた。しかしこの2つは排他的な操作で、両方同時に実行する意味がない。チェックボックスだとユーザーが両方ONにできてしまう。",[15,566,567],{},"ラジオボタンに変更して、出力先を1つだけ選ぶUIにした。小さな変更だが、操作の迷いが消えた。",[19,569],{},[22,571,572],{"id":572},"ページ遷移の待機ロジックを書き直す",[15,574,575,576,579],{},"この拡張は複数の口座を順番に切り替えながら明細を取得する。口座切り替え時にページ遷移が発生し、DOMが再構築される。元の実装では固定1.5秒の ",[30,577,578],{},"setTimeout"," で待機していた。",[39,581,582],{"id":582},"固定待機の問題",[15,584,585],{},"最後の口座に切り替えた直後、フィルタ条件が適用される前のデータを拾ってしまう現象が出た。ネットワークが遅いタイミングで1.5秒では足りず、前の口座のデータがまだ表示されている状態でスクレイピングが走っていた。",[39,587,589],{"id":588},"修正-条件ベースの待機","修正: 条件ベースの待機",[15,591,592],{},"固定秒数の待機を捨てて、2つの条件が揃うまでポーリングする方式に変えた。",[393,594,595,601],{},[396,596,597,600],{},[58,598,599],{},"テーブル要素が存在する","（DOMの再構築完了）",[396,602,603,606],{},[58,604,605],{},"ドロップダウンに対象口座名が表示されている","（フィルタ適用完了）",[63,608,610],{"className":265,"code":609,"language":267,"meta":68,"style":68},"const waitForAccountReady = async (accountName, maxWait = 15000) => {\n  const start = Date.now();\n  while (Date.now() - start \u003C maxWait) {\n    const table = document.querySelector('.data-table');\n    const dropdown = document.querySelector('.account-selector');\n    if (table && dropdown?.textContent.includes(accountName)) {\n      return true;\n    }\n    await new Promise(r => setTimeout(r, 300));\n  }\n  throw new Error(`Timeout: ${accountName} の読み込みが${maxWait}ms以内に完了しなかった`);\n};\n",[30,611,612,646,667,698,729,755,790,800,806,839,844,885],{"__ignoreMap":68},[72,613,614,616,619,621,624,626,629,631,634,636,640,642,644],{"class":74,"line":75},[72,615,280],{"class":279},[72,617,618],{"class":428}," waitForAccountReady",[72,620,287],{"class":85},[72,622,623],{"class":279}," async",[72,625,434],{"class":85},[72,627,628],{"class":283},"accountName",[72,630,440],{"class":85},[72,632,633],{"class":283}," maxWait",[72,635,287],{"class":85},[72,637,639],{"class":638},"sM54T"," 15000",[72,641,446],{"class":85},[72,643,535],{"class":85},[72,645,105],{"class":85},[72,647,648,651,654,656,659,661,664],{"class":74,"line":82},[72,649,650],{"class":279},"  const",[72,652,653],{"class":283}," start",[72,655,287],{"class":85},[72,657,658],{"class":283}," Date",[72,660,457],{"class":85},[72,662,663],{"class":428},"now",[72,665,666],{"class":85},"();\n",[72,668,669,672,674,677,679,681,684,687,689,692,694,696],{"class":74,"line":89},[72,670,671],{"class":296},"  while",[72,673,434],{"class":85},[72,675,676],{"class":283},"Date",[72,678,457],{"class":85},[72,680,663],{"class":428},[72,682,683],{"class":85},"()",[72,685,686],{"class":279}," -",[72,688,653],{"class":283},[72,690,691],{"class":85}," \u003C",[72,693,633],{"class":283},[72,695,446],{"class":85},[72,697,105],{"class":85},[72,699,700,703,706,708,711,713,716,718,721,724,726],{"class":74,"line":108},[72,701,702],{"class":279},"    const",[72,704,705],{"class":283}," table",[72,707,287],{"class":85},[72,709,710],{"class":283}," document",[72,712,457],{"class":85},[72,714,715],{"class":428},"querySelector",[72,717,463],{"class":85},[72,719,720],{"class":121},"'",[72,722,723],{"class":125},".data-table",[72,725,720],{"class":121},[72,727,728],{"class":85},");\n",[72,730,731,733,736,738,740,742,744,746,748,751,753],{"class":74,"line":134},[72,732,702],{"class":279},[72,734,735],{"class":283}," dropdown",[72,737,287],{"class":85},[72,739,710],{"class":283},[72,741,457],{"class":85},[72,743,715],{"class":428},[72,745,463],{"class":85},[72,747,720],{"class":121},[72,749,750],{"class":125},".account-selector",[72,752,720],{"class":121},[72,754,728],{"class":85},[72,756,757,760,762,765,768,770,773,776,778,781,783,785,788],{"class":74,"line":159},[72,758,759],{"class":296},"    if",[72,761,434],{"class":85},[72,763,764],{"class":283},"table",[72,766,767],{"class":279}," &&",[72,769,735],{"class":283},[72,771,772],{"class":85},"?.",[72,774,775],{"class":283},"textContent",[72,777,457],{"class":85},[72,779,780],{"class":428},"includes",[72,782,463],{"class":85},[72,784,628],{"class":283},[72,786,787],{"class":85},"))",[72,789,105],{"class":85},[72,791,792,795,798],{"class":74,"line":165},[72,793,794],{"class":296},"      return",[72,796,797],{"class":296}," true",[72,799,319],{"class":85},[72,801,803],{"class":74,"line":802},8,[72,804,805],{"class":85},"    }\n",[72,807,809,812,815,818,820,823,825,828,830,832,834,837],{"class":74,"line":808},9,[72,810,811],{"class":296},"    await",[72,813,814],{"class":279}," new",[72,816,817],{"class":96}," Promise",[72,819,463],{"class":85},[72,821,822],{"class":283},"r",[72,824,535],{"class":85},[72,826,827],{"class":428}," setTimeout",[72,829,463],{"class":85},[72,831,822],{"class":283},[72,833,440],{"class":85},[72,835,836],{"class":638}," 300",[72,838,552],{"class":85},[72,840,842],{"class":74,"line":841},10,[72,843,162],{"class":85},[72,845,847,850,852,855,857,859,862,864,866,868,871,873,876,878,881,883],{"class":74,"line":846},11,[72,848,849],{"class":296},"  throw",[72,851,814],{"class":279},[72,853,854],{"class":428}," Error",[72,856,463],{"class":85},[72,858,316],{"class":121},[72,860,861],{"class":125},"Timeout: ",[72,863,297],{"class":296},[72,865,628],{"class":125},[72,867,303],{"class":296},[72,869,870],{"class":125}," の読み込みが",[72,872,297],{"class":296},[72,874,875],{"class":125},"maxWait",[72,877,303],{"class":296},[72,879,880],{"class":125},"ms以内に完了しなかった",[72,882,316],{"class":121},[72,884,728],{"class":85},[72,886,888],{"class":74,"line":887},12,[72,889,890],{"class":85},"};\n",[15,892,893],{},"「テーブルが見える」だけでは不十分で、「正しい口座のデータが表示されている」まで確認する必要があった。固定秒数の待機は、遅いときに足りず、速いときに無駄に待つ。条件ベースなら両方解決する。",[19,895],{},[22,897,898],{"id":898},"振り返り",[15,900,901],{},"半日で5つのエラーを踏んだ。OAuth2のクライアントIDタイプ、host_permissions漏れ、API URLの違い、全上書きの危険性、固定待機の不安定さ。どれも「動かしてみて初めてわかる」類のもので、ドキュメントを読むだけでは防げなかった。",[15,903,904],{},"1つだけ事前に防げたのがhost_permissionsで、これはCodexレビューのおかげだった。自分のコードを別の目で見てもらう仕組みは、たとえAIであっても機能する。",[15,906,907],{},"Chrome拡張 + Google Sheets APIの組み合わせは、認証の設定さえ乗り越えれば、スプレッドシートをデータベースのように使える。個人ツールとしてのコスパは高い。",[909,910,911],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sqvqQ, html code.shiki .sqvqQ{--shiki-default:#99841877;--shiki-dark:#99841877}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}",{"title":68,"searchDepth":82,"depth":82,"links":913},[914,917,918,919,922,923,927],{"id":24,"depth":82,"text":25,"children":915},[916],{"id":41,"depth":89,"text":42},{"id":180,"depth":82,"text":181},{"id":247,"depth":82,"text":248},{"id":384,"depth":82,"text":385,"children":920},[921],{"id":391,"depth":89,"text":391},{"id":560,"depth":82,"text":561},{"id":572,"depth":82,"text":572,"children":924},[925,926],{"id":582,"depth":89,"text":582},{"id":588,"depth":89,"text":589},{"id":898,"depth":82,"text":898},"dev","会計ソフトの明細取得Chrome拡張にGoogle Sheets API連携を実装。OAuth2クライアントIDの罠、API URLの違い、差分追記ロジック、ページ遷移待機の改善まで記録","md",{},null,"/chrome-extension-sheets-api","misc-dev",false,"2026-03-22T00:00:00.000Z",{"title":5,"description":929},"2026-03/2026-03-22/chrome-extension-sheets-api",[940,941,942,943],"Chrome拡張機能","Google Sheets API","OAuth2","manifest.json","memo","lIISvcaGU2FENaI_CmlYLdaa2JkpKfj89BbyJ-AL0tg",[],"https://log.eurekapu.com/og/blog/chrome-extension-sheets-api.png?v=2026-03-22T00%3A00%3A00.000Z&title=Chrome%E6%8B%A1%E5%BC%B5%E6%A9%9F%E8%83%BD%E3%81%ABGoogle%20Sheets%20API%E6%9B%B8%E3%81%8D%E8%BE%BC%E3%81%BF%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B%20-%20OAuth2%E8%AA%8D%E8%A8%BC%E3%81%8B%E3%82%89diff%E8%BF%BD%E8%A8%98%E3%81%BE%E3%81%A7&author=Kei%20Komatsu&sig=7161016126216ba5",1782528819793]