[{"data":1,"prerenderedAt":1030},["ShallowReactive",2],{"content-/x-bookmark-exporter-oauth-sheets":3,"all-pages-for-dir":1028,"og-image-/x-bookmark-exporter-oauth-sheets":1029},{"id":4,"title":5,"body":6,"category":1007,"description":1008,"extension":1009,"meta":1010,"navigation":245,"path":1011,"project_name":1012,"published":1013,"publishedAt":1014,"seo":1015,"stem":1016,"tags":1017,"todo":1025,"updatedAt":1026,"__hash__":1027},"pages/2026-04/2026-04-06/x-bookmark-exporter-oauth-sheets.md","Chrome拡張機能: Xブックマーク分析基盤の構築 — OAuth2認証・Sheets API連携・SQLite変換",{"type":7,"value":8,"toc":975},"minimark",[9,14,18,21,26,30,38,50,54,57,165,172,174,178,182,193,290,293,295,299,303,306,511,514,516,520,524,531,535,538,542,545,547,551,555,562,583,869,871,874,877,880,884,887,903,905,909,913,916,923,925,929,933,936,940,943,957,960,962,965,968,971],[10,11,13],"h1",{"id":12},"chrome拡張機能-xブックマーク分析基盤の構築","Chrome拡張機能: Xブックマーク分析基盤の構築",[15,16,17],"p",{},"前日に設計・実装した X Bookmark Exporter を実際に動かし始めたら、OAuth2の認証画面が出ない。ターミナルにエラーが返り、ポップアップは無反応のまま固まっている。原因を追い始めたところ、client IDが別の拡張機能に紐付けられていた。ここから「動くまで持っていく」作業と、動いた後の「8,000件超のブックマークをどう料理するか」という分析作業が始まった。",[19,20],"hr",{},[22,23,25],"h2",{"id":24},"oauth2-client-idの罠-拡張機能idとの1対1紐付け","OAuth2 client IDの罠: 拡張機能IDとの1対1紐付け",[27,28,29],"h3",{"id":29},"何が起きたか",[15,31,32,33,37],{},"会計ソフトA用Chrome拡張のOAuth2パターンを流用する設計だった。manifest.jsonの",[34,35,36],"code",{},"oauth2","セクションをコピペして、スコープだけ変えれば動くはず――と思っていた。",[15,39,40,41,44,45,49],{},"しかし",[34,42,43],{},"chrome.identity.getAuthToken()","を呼んでも認証ダイアログが出ない。Chrome拡張のOAuth2では、Google Cloud Consoleで作成したclient IDが",[46,47,48],"strong",{},"拡張機能ID（32文字の英小文字）と1対1で紐付く","。会計ソフトA用のclient IDはあちらの拡張機能IDに結びついているので、別の拡張機能からは使えない。",[27,51,53],{"id":52},"解決-新規client-id作成","解決: 新規client ID作成",[15,55,56],{},"Google Cloud Consoleで「Chrome拡張機能」タイプのOAuth 2.0クライアントIDを新規作成し、この拡張機能のIDを登録した。manifest.jsonのclient_idを差し替えたら、認証ダイアログが表示されてトークンが返ってきた。",[58,59,64],"pre",{"className":60,"code":61,"language":62,"meta":63,"style":63},"language-json shiki shiki-themes vitesse-light vitesse-light","{\n  \"oauth2\": {\n    \"client_id\": \"xxxx-新規作成したID.apps.googleusercontent.com\",\n    \"scopes\": [\n      \"https://www.googleapis.com/auth/spreadsheets\"\n    ]\n  }\n}\n","json","",[34,65,66,75,94,120,135,147,153,159],{"__ignoreMap":63},[67,68,71],"span",{"class":69,"line":70},"line",1,[67,72,74],{"class":73},"shFtX","{\n",[67,76,78,82,85,88,91],{"class":69,"line":77},2,[67,79,81],{"class":80},"sqvqQ","  \"",[67,83,36],{"class":84},"sz8Xr",[67,86,87],{"class":80},"\"",[67,89,90],{"class":73},":",[67,92,93],{"class":73}," {\n",[67,95,97,100,103,105,107,111,115,117],{"class":69,"line":96},3,[67,98,99],{"class":80},"    \"",[67,101,102],{"class":84},"client_id",[67,104,87],{"class":80},[67,106,90],{"class":73},[67,108,110],{"class":109},"sMJiu"," \"",[67,112,114],{"class":113},"sdGka","xxxx-新規作成したID.apps.googleusercontent.com",[67,116,87],{"class":109},[67,118,119],{"class":73},",\n",[67,121,123,125,128,130,132],{"class":69,"line":122},4,[67,124,99],{"class":80},[67,126,127],{"class":84},"scopes",[67,129,87],{"class":80},[67,131,90],{"class":73},[67,133,134],{"class":73}," [\n",[67,136,138,141,144],{"class":69,"line":137},5,[67,139,140],{"class":109},"      \"",[67,142,143],{"class":113},"https://www.googleapis.com/auth/spreadsheets",[67,145,146],{"class":109},"\"\n",[67,148,150],{"class":69,"line":149},6,[67,151,152],{"class":73},"    ]\n",[67,154,156],{"class":69,"line":155},7,[67,157,158],{"class":73},"  }\n",[67,160,162],{"class":69,"line":161},8,[67,163,164],{"class":73},"}\n",[15,166,167,168,171],{},"教訓: ",[34,169,170],{},"chrome.identity","のOAuth2パターン自体は再利用できるが、client IDはプロジェクトごとに別途作る必要がある。コードの流用とclient IDの流用は別物。",[19,173],{},[22,175,177],{"id":176},"screen_name-display_nameの取得パス修正","screen_name / display_nameの取得パス修正",[27,179,181],{"id":180},"x-apiの構造変更に遭遇","X APIの構造変更に遭遇",[15,183,184,185,188,189,192],{},"ブックマークのスクレーピングでユーザー情報を取得する部分が、空文字を返してくる。DevToolsのNetworkタブでレスポンスのJSONを開いて目視で辿ったところ、以前は ",[34,186,187],{},"user_results.result.legacy"," にあったscreen_nameとdisplay_nameが、",[34,190,191],{},"user_results.result.core"," 配下に移動していた。",[58,194,198],{"className":195,"code":196,"language":197,"meta":63,"style":63},"language-javascript shiki shiki-themes vitesse-light vitesse-light","// Before: legacy パスに格納されていた\nconst screenName = user.result.legacy.screen_name;\n\n// After: core パスに移動\nconst screenName = user.result.core.user_results.result.legacy.screen_name;\n","javascript",[34,199,200,206,241,247,252],{"__ignoreMap":63},[67,201,202],{"class":69,"line":70},[67,203,205],{"class":204},"sxvE3","// Before: legacy パスに格納されていた\n",[67,207,208,212,216,219,222,225,228,230,233,235,238],{"class":69,"line":77},[67,209,211],{"class":210},"stQ0i","const",[67,213,215],{"class":214},"s4oTP"," screenName",[67,217,218],{"class":73}," =",[67,220,221],{"class":214}," user",[67,223,224],{"class":73},".",[67,226,227],{"class":214},"result",[67,229,224],{"class":73},[67,231,232],{"class":214},"legacy",[67,234,224],{"class":73},[67,236,237],{"class":214},"screen_name",[67,239,240],{"class":73},";\n",[67,242,243],{"class":69,"line":96},[67,244,246],{"emptyLinePlaceholder":245},true,"\n",[67,248,249],{"class":69,"line":122},[67,250,251],{"class":204},"// After: core パスに移動\n",[67,253,254,256,258,260,262,264,266,268,271,273,276,278,280,282,284,286,288],{"class":69,"line":137},[67,255,211],{"class":210},[67,257,215],{"class":214},[67,259,218],{"class":73},[67,261,221],{"class":214},[67,263,224],{"class":73},[67,265,227],{"class":214},[67,267,224],{"class":73},[67,269,270],{"class":214},"core",[67,272,224],{"class":73},[67,274,275],{"class":214},"user_results",[67,277,224],{"class":73},[67,279,227],{"class":214},[67,281,224],{"class":73},[67,283,232],{"class":214},[67,285,224],{"class":73},[67,287,237],{"class":214},[67,289,240],{"class":73},[15,291,292],{},"X（旧Twitter）のAPI構造は予告なく変わる。セレクタやパスが壊れたときに「何が空なのか」をログに出す防御コードを追加した。",[19,294],{},[22,296,298],{"id":297},"sheets-apiバッチ書き込み-6000件対応","Sheets APIバッチ書き込み: 6,000件対応",[27,300,302],{"id":301},"_500行ずつ分割して書き込む","500行ずつ分割して書き込む",[15,304,305],{},"ブックマークが数千件規模になると、Sheets APIの1回のリクエストに全行を詰め込むとタイムアウトする。500行ずつに分割してバッチ書き込みする方式にした。",[58,307,309],{"className":195,"code":308,"language":197,"meta":63,"style":63},"const BATCH_SIZE = 500;\nfor (let i = 0; i \u003C rows.length; i += BATCH_SIZE) {\n  const batch = rows.slice(i, i + BATCH_SIZE);\n  await sheets.spreadsheets.values.append({\n    spreadsheetId,\n    range: 'Sheet1',\n    valueInputOption: 'RAW',\n    resource: { values: batch }\n  });\n}\n",[34,310,311,326,376,413,439,446,464,480,500,506],{"__ignoreMap":63},[67,312,313,315,318,320,324],{"class":69,"line":70},[67,314,211],{"class":210},[67,316,317],{"class":214}," BATCH_SIZE",[67,319,218],{"class":73},[67,321,323],{"class":322},"sM54T"," 500",[67,325,240],{"class":73},[67,327,328,332,335,338,341,343,346,349,351,354,357,359,362,364,366,369,371,374],{"class":69,"line":77},[67,329,331],{"class":330},"sHkkW","for",[67,333,334],{"class":73}," (",[67,336,337],{"class":210},"let",[67,339,340],{"class":214}," i",[67,342,218],{"class":73},[67,344,345],{"class":322}," 0",[67,347,348],{"class":73},";",[67,350,340],{"class":214},[67,352,353],{"class":73}," \u003C",[67,355,356],{"class":214}," rows",[67,358,224],{"class":73},[67,360,361],{"class":84},"length",[67,363,348],{"class":73},[67,365,340],{"class":214},[67,367,368],{"class":210}," +=",[67,370,317],{"class":214},[67,372,373],{"class":73},")",[67,375,93],{"class":73},[67,377,378,381,384,386,388,390,394,397,400,403,405,408,410],{"class":69,"line":96},[67,379,380],{"class":210},"  const",[67,382,383],{"class":214}," batch",[67,385,218],{"class":73},[67,387,356],{"class":214},[67,389,224],{"class":73},[67,391,393],{"class":392},"senZ8","slice",[67,395,396],{"class":73},"(",[67,398,399],{"class":214},"i",[67,401,402],{"class":73},",",[67,404,340],{"class":214},[67,406,407],{"class":210}," +",[67,409,317],{"class":214},[67,411,412],{"class":73},");\n",[67,414,415,418,421,423,426,428,431,433,436],{"class":69,"line":122},[67,416,417],{"class":330},"  await",[67,419,420],{"class":214}," sheets",[67,422,224],{"class":73},[67,424,425],{"class":214},"spreadsheets",[67,427,224],{"class":73},[67,429,430],{"class":214},"values",[67,432,224],{"class":73},[67,434,435],{"class":392},"append",[67,437,438],{"class":73},"({\n",[67,440,441,444],{"class":69,"line":137},[67,442,443],{"class":214},"    spreadsheetId",[67,445,119],{"class":73},[67,447,448,451,453,456,459,462],{"class":69,"line":149},[67,449,450],{"class":84},"    range",[67,452,90],{"class":73},[67,454,455],{"class":109}," '",[67,457,458],{"class":113},"Sheet1",[67,460,461],{"class":109},"'",[67,463,119],{"class":73},[67,465,466,469,471,473,476,478],{"class":69,"line":155},[67,467,468],{"class":84},"    valueInputOption",[67,470,90],{"class":73},[67,472,455],{"class":109},[67,474,475],{"class":113},"RAW",[67,477,461],{"class":109},[67,479,119],{"class":73},[67,481,482,485,487,490,493,495,497],{"class":69,"line":161},[67,483,484],{"class":84},"    resource",[67,486,90],{"class":73},[67,488,489],{"class":73}," {",[67,491,492],{"class":84}," values",[67,494,90],{"class":73},[67,496,383],{"class":214},[67,498,499],{"class":73}," }\n",[67,501,503],{"class":69,"line":502},9,[67,504,505],{"class":73},"  });\n",[67,507,509],{"class":69,"line":508},10,[67,510,164],{"class":73},[15,512,513],{},"6,000件のブックマークを12回のバッチで書き込めた。ポップアップには進捗表示（「500/6000件書き込み中...」）を追加して、処理が止まっていないことが目で見えるようにした。",[19,515],{},[22,517,519],{"id":518},"ui改善-ブックマークページ以外からの実行対応","UI改善: ブックマークページ以外からの実行対応",[27,521,523],{"id":522},"問題-ブックマークページを開いていないと動かない","問題: ブックマークページを開いていないと動かない",[15,525,526,527,530],{},"当初の実装では、ユーザーがXのブックマークページ（",[34,528,529],{},"x.com/i/bookmarks","）を開いた状態でポップアップの実行ボタンを押す必要があった。他のページを見ているときに「あ、ブックマークをエクスポートしたい」と思ったら、まずブックマークページに手動で移動する手間がある。",[27,532,534],{"id":533},"解決-自動でタブを開いて完了後に閉じる","解決: 自動でタブを開いて完了後に閉じる",[15,536,537],{},"ブックマークページ以外から実行ボタンを押した場合、background.jsが自動でブックマークページのタブを新規作成し、スクレーピング完了後にそのタブを閉じる処理を入れた。ユーザーの手元の作業が中断されない。",[27,539,541],{"id":540},"テスト用10件csvボタン","テスト用10件CSVボタン",[15,543,544],{},"開発中は毎回6,000件をスクレーピングすると時間がかかる。ポップアップに「テスト: 10件CSV出力」ボタンを一時的に追加し、スクレーピングの上限を10件に制限してCSVを吐くモードで動作確認を繰り返した。動作が安定したところでこのボタンは削除した。",[19,546],{},[22,548,550],{"id":549},"backgroundjsのsendresponseエラー修正","background.jsのsendResponseエラー修正",[27,552,554],{"id":553},"promiseのcatch漏れ","Promiseのcatch漏れ",[15,556,557,558,561],{},"ポップアップからbackground.jsにメッセージを送り、非同期処理の結果を",[34,559,560],{},"sendResponse","で返すパターンで、コンソールにエラーが出ていた。",[15,563,564,565,568,569,571,572,575,576,582],{},"原因は、非同期処理をPromiseチェーンで書いていたが、",[34,566,567],{},".catch()","でエラーを拾った際に",[34,570,560],{},"を呼んでいなかったこと。Chrome拡張のメッセージングでは、",[34,573,574],{},"return true;","で非同期応答を有効にした場合、",[46,577,578,579,581],{},"全てのコードパスで",[34,580,560],{},"を呼ぶ","必要がある。",[58,584,586],{"className":195,"code":585,"language":197,"meta":63,"style":63},"// Before: catchでsendResponseを呼んでいない\nchrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {\n  doAsyncWork()\n    .then(result => sendResponse({ success: true, data: result }))\n    .catch(err => console.error(err)); // ← sendResponse漏れ\n  return true;\n});\n\n// After: catchでもsendResponseを返す\nchrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {\n  doAsyncWork()\n    .then(result => sendResponse({ success: true, data: result }))\n    .catch(err => sendResponse({ success: false, error: err.message }));\n  return true;\n});\n",[34,587,588,593,636,644,684,716,725,730,734,739,773,780,813,855,864],{"__ignoreMap":63},[67,589,590],{"class":69,"line":70},[67,591,592],{"class":204},"// Before: catchでsendResponseを呼んでいない\n",[67,594,595,598,600,603,605,608,610,613,616,619,621,624,626,629,631,634],{"class":69,"line":77},[67,596,597],{"class":214},"chrome",[67,599,224],{"class":73},[67,601,602],{"class":214},"runtime",[67,604,224],{"class":73},[67,606,607],{"class":214},"onMessage",[67,609,224],{"class":73},[67,611,612],{"class":392},"addListener",[67,614,615],{"class":73},"((",[67,617,618],{"class":214},"msg",[67,620,402],{"class":73},[67,622,623],{"class":214}," sender",[67,625,402],{"class":73},[67,627,628],{"class":214}," sendResponse",[67,630,373],{"class":73},[67,632,633],{"class":73}," =>",[67,635,93],{"class":73},[67,637,638,641],{"class":69,"line":96},[67,639,640],{"class":392},"  doAsyncWork",[67,642,643],{"class":73},"()\n",[67,645,646,649,652,654,656,658,660,663,666,668,671,673,676,678,681],{"class":69,"line":122},[67,647,648],{"class":73},"    .",[67,650,651],{"class":392},"then",[67,653,396],{"class":73},[67,655,227],{"class":214},[67,657,633],{"class":73},[67,659,628],{"class":392},[67,661,662],{"class":73},"({",[67,664,665],{"class":84}," success",[67,667,90],{"class":73},[67,669,670],{"class":330}," true",[67,672,402],{"class":73},[67,674,675],{"class":84}," data",[67,677,90],{"class":73},[67,679,680],{"class":214}," result",[67,682,683],{"class":73}," }))\n",[67,685,686,688,691,693,696,698,701,703,706,708,710,713],{"class":69,"line":137},[67,687,648],{"class":73},[67,689,690],{"class":392},"catch",[67,692,396],{"class":73},[67,694,695],{"class":214},"err",[67,697,633],{"class":73},[67,699,700],{"class":214}," console",[67,702,224],{"class":73},[67,704,705],{"class":392},"error",[67,707,396],{"class":73},[67,709,695],{"class":214},[67,711,712],{"class":73},"));",[67,714,715],{"class":204}," // ← sendResponse漏れ\n",[67,717,718,721,723],{"class":69,"line":149},[67,719,720],{"class":330},"  return",[67,722,670],{"class":330},[67,724,240],{"class":73},[67,726,727],{"class":69,"line":155},[67,728,729],{"class":73},"});\n",[67,731,732],{"class":69,"line":161},[67,733,246],{"emptyLinePlaceholder":245},[67,735,736],{"class":69,"line":502},[67,737,738],{"class":204},"// After: catchでもsendResponseを返す\n",[67,740,741,743,745,747,749,751,753,755,757,759,761,763,765,767,769,771],{"class":69,"line":508},[67,742,597],{"class":214},[67,744,224],{"class":73},[67,746,602],{"class":214},[67,748,224],{"class":73},[67,750,607],{"class":214},[67,752,224],{"class":73},[67,754,612],{"class":392},[67,756,615],{"class":73},[67,758,618],{"class":214},[67,760,402],{"class":73},[67,762,623],{"class":214},[67,764,402],{"class":73},[67,766,628],{"class":214},[67,768,373],{"class":73},[67,770,633],{"class":73},[67,772,93],{"class":73},[67,774,776,778],{"class":69,"line":775},11,[67,777,640],{"class":392},[67,779,643],{"class":73},[67,781,783,785,787,789,791,793,795,797,799,801,803,805,807,809,811],{"class":69,"line":782},12,[67,784,648],{"class":73},[67,786,651],{"class":392},[67,788,396],{"class":73},[67,790,227],{"class":214},[67,792,633],{"class":73},[67,794,628],{"class":392},[67,796,662],{"class":73},[67,798,665],{"class":84},[67,800,90],{"class":73},[67,802,670],{"class":330},[67,804,402],{"class":73},[67,806,675],{"class":84},[67,808,90],{"class":73},[67,810,680],{"class":214},[67,812,683],{"class":73},[67,814,816,818,820,822,824,826,828,830,832,834,837,839,842,844,847,849,852],{"class":69,"line":815},13,[67,817,648],{"class":73},[67,819,690],{"class":392},[67,821,396],{"class":73},[67,823,695],{"class":214},[67,825,633],{"class":73},[67,827,628],{"class":392},[67,829,662],{"class":73},[67,831,665],{"class":84},[67,833,90],{"class":73},[67,835,836],{"class":330}," false",[67,838,402],{"class":73},[67,840,841],{"class":84}," error",[67,843,90],{"class":73},[67,845,846],{"class":214}," err",[67,848,224],{"class":73},[67,850,851],{"class":214},"message",[67,853,854],{"class":73}," }));\n",[67,856,858,860,862],{"class":69,"line":857},14,[67,859,720],{"class":330},[67,861,670],{"class":330},[67,863,240],{"class":73},[67,865,867],{"class":69,"line":866},15,[67,868,729],{"class":73},[19,870],{},[22,872,873],{"id":873},"リネームとリファクタリング",[27,875,876],{"id":876},"拡張機能名の変更",[15,878,879],{},"動画ダウンロード機能とブックマークエクスポート機能の両方を持つようになったため、拡張機能名を「X Video & Bookmark Exporter」に変更した。",[27,881,883],{"id":882},"simplifyリファクタリング","Simplifyリファクタリング",[15,885,886],{},"コードが膨らんできたので整理を入れた。",[888,889,890,897],"ul",{},[891,892,893,896],"li",{},[46,894,895],{},"content.js",": スクレーピングで使うCSSセレクタやAPI構造のパス文字列をファイル冒頭に定数として抽出",[891,898,899,902],{},[46,900,901],{},"background.js",": フォルダ管理用HTMLの重複テンプレートを共通関数に統合",[19,904],{},[22,906,908],{"id":907},"ブックマーク8251件をsqliteに変換","ブックマーク8,251件をSQLiteに変換",[27,910,912],{"id":911},"csv-sqlite変換","CSV → SQLite変換",[15,914,915],{},"Sheets APIで出力したデータをCSVでも保存していたので、これをSQLiteに変換してbookmarks.dbを構築した。テーブルは1つで、tweet_id, author, screen_name, content, created_at, url, tagsカラムを持つ。",[15,917,918,919,922],{},"8,251件のブックマークが1つのDBファイルに収まった。",[34,920,921],{},"SELECT * FROM bookmarks WHERE content LIKE '%Claude%'","のようなクエリで即座に検索できる。スクロールを繰り返して目視で探していた頃とは比べものにならない。",[19,924],{},[22,926,928],{"id":927},"年度別ブックマーク分析レポート-9年分の興味関心の遷移","年度別ブックマーク分析レポート: 9年分の興味関心の遷移",[27,930,932],{"id":931},"_2018年2026年の年度別レポートを生成","2018年〜2026年の年度別レポートを生成",[15,934,935],{},"bookmarks.dbに対して年度ごとの集計クエリを走らせ、各年のブックマーク傾向をMermaid図付きでレポートにまとめた。9年分の年度別レポートを一気に生成した。",[27,937,939],{"id":938},"_2023年を境に変わる景色","2023年を境に変わる景色",[15,941,942],{},"データを並べると、2023年以前と以降で明らかにブックマークの傾向が変わっている。",[888,944,945,951],{},[891,946,947,950],{},[46,948,949],{},"2018〜2022年",": プログラミング全般、Web開発、会計・税務、雑多な技術情報",[891,952,953,956],{},[46,954,955],{},"2023年〜",": ChatGPTの登場以降、AI・LLM・プロンプトエンジニアリング関連のブックマークが急増",[15,958,959],{},"2022年まではカテゴリが均等に散らばっていたのが、2023年を境にAI系タグがブックマーク全体の4割を超えた。自分では「まんべんなく情報を追っている」つもりだったが、数字は正直だった。興味関心がAIに吸い寄せられていく軌跡が、ブックマークのタイムスタンプに刻まれていた。",[19,961],{},[22,963,964],{"id":964},"学びメモ",[15,966,967],{},"Chrome拡張のOAuth2 client IDが拡張機能IDと1対1で紐付く仕組みは、ドキュメントを読んでも頭に入らなかった。会計ソフトA拡張のclient IDをコピペして認証画面が出ない状況に15分ほど向き合い、Cloud Consoleの設定画面を開いて「あ、ここにアプリケーションIDが登録されている」と目で確認して初めて理解した。",[15,969,970],{},"8,251件のブックマークをSQLiteに入れて年度別に集計したとき、ターミナルに数字が並ぶのを眺めて手が止まった。自分の9年間の関心の流れが、GROUP BYの結果として返ってくる。「2023年にChatGPTに出会って、そこからAIに没頭した」という自覚はあったが、ブックマーク数の推移グラフで裏付けられると、なんというか、動かぬ証拠を突きつけられた気分になる。",[972,973,974],"style",{},"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 .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}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 .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}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}",{"title":63,"searchDepth":77,"depth":77,"links":976},[977,981,984,987,992,995,999,1002,1006],{"id":24,"depth":77,"text":25,"children":978},[979,980],{"id":29,"depth":96,"text":29},{"id":52,"depth":96,"text":53},{"id":176,"depth":77,"text":177,"children":982},[983],{"id":180,"depth":96,"text":181},{"id":297,"depth":77,"text":298,"children":985},[986],{"id":301,"depth":96,"text":302},{"id":518,"depth":77,"text":519,"children":988},[989,990,991],{"id":522,"depth":96,"text":523},{"id":533,"depth":96,"text":534},{"id":540,"depth":96,"text":541},{"id":549,"depth":77,"text":550,"children":993},[994],{"id":553,"depth":96,"text":554},{"id":873,"depth":77,"text":873,"children":996},[997,998],{"id":876,"depth":96,"text":876},{"id":882,"depth":96,"text":883},{"id":907,"depth":77,"text":908,"children":1000},[1001],{"id":911,"depth":96,"text":912},{"id":927,"depth":77,"text":928,"children":1003},[1004,1005],{"id":931,"depth":96,"text":932},{"id":938,"depth":96,"text":939},{"id":964,"depth":77,"text":964},"dev","X Bookmark ExporterのOAuth2 client ID問題の解決、screen_name取得パス修正、Sheets APIバッチ書き込み実装、ブックマーク8,251件のSQLite化と年度別分析レポート生成までの一日の記録","md",{},"/x-bookmark-exporter-oauth-sheets","chrome-extension-x",false,"2026-04-06T00:00:00.000Z",{"title":5,"description":1008},"2026-04/2026-04-06/x-bookmark-exporter-oauth-sheets",[1018,1019,1020,1021,1022,1023,1024],"Chrome拡張機能","X","Google Sheets API","OAuth2","SQLite","Mermaid","データ分析","memo",null,"_Sf05rmKpuSKz_SQwJMVDeGN_XohOwbJmwdUCOWD4aM",[],"https://log.eurekapu.com/favicon.svg",1775511575196]