[{"data":1,"prerenderedAt":945},["ShallowReactive",2],{"search-dialog-posts":3,"post-\u002Fposts\u002Fgoogle-app-about-blank-placeholder":316,"post-surround-\u002Fposts\u002Fgoogle-app-about-blank-placeholder":938},[4,12,21,29,37,47,57,67,77,85,96,104,112,121,129,137,147,155,164,173,181,188,195,207,215,224,236,241,247,254,260,266,272,279,286,291,298,304,310],{"path":5,"title":6,"description":7,"category":8,"tags":9},"\u002Fposts\u002Fgit-commands-cheatsheet","Git 常用指令與終端機筆記","2018 年整理的 Git 學習筆記，包含基本終端機指令、Git 操作、分支、reset、SourceTree、Vim 與一些常用設定。","notes",[10,11],"Git","版本控制",{"path":13,"title":14,"description":15,"category":16,"tags":17},"\u002Fposts\u002Fjs-prototype-inheritance","JavaScript 物件導向：建構函式、prototype 與繼承三步驟","整理 ES5 時代用建構函式加 prototype 手刻物件導向的完整流程——封裝屬性與方法、共用函式為什麼要掛在 prototype 上、以及繼承的三個步驟（call 借用母類別建構子、Object.create 接上原型鏈、修正 constructor 指向），並附上物件縮寫與計算屬性這兩個順便學到的 ES6 寫法。","frontend",[18,19,20],"JavaScript","物件導向","prototype",{"path":22,"title":23,"description":24,"category":16,"tags":25},"\u002Fposts\u002Fflexbox-notes","Flexbox 對齊與彈性尺寸筆記","記錄 Flexbox 的主軸與交錯軸對齊、flex-direction、flex-wrap，以及 flex-grow、flex-shrink、flex-basis 等常用屬性。",[26,27,28],"CSS","Flexbox","版面",{"path":30,"title":31,"description":32,"category":16,"tags":33},"\u002Fposts\u002Fscreen-orientation-api","screen.orientation：現在判斷與鎖定螢幕方向的做法","整理現在 JavaScript 判斷螢幕方向的方式，包括 screen.orientation、change 事件與 lock()，並說明 RWD 使用 matchMedia 或 CSS orientation 會更適合。",[18,34,35,36],"RWD","螢幕方向","PWA",{"path":38,"title":39,"description":40,"category":16,"tags":41},"\u002Fposts\u002Fpinia-vuex-state-management","Pinia \u002F Vuex 狀態管理及差異","紀錄 Vuex、Pinia 的狀態管理概念、差異與用法。",[42,43,44,45,46],"Pinia","Vuex","Vue3","狀態管理","Nuxt",{"path":48,"title":49,"description":50,"category":8,"tags":51},"\u002Fposts\u002Fpyenv-python-version-management","Python 多版本管理 pyenv","紀錄 macOS 與 Windows 使用 pyenv 管理 Python 版本的方式。",[52,53,54,55,56],"Python","pyenv","macOS","Windows","版本管理",{"path":58,"title":59,"description":60,"category":8,"tags":61},"\u002Fposts\u002Fnpm-publish-vue2-component","npm 發布流程","紀錄 Vue 2 元件的 npm 發布、dist-tag 與打包流程。",[62,63,64,65,66],"npm","Vue2","Vue CLI","套件發布","MIT",{"path":68,"title":69,"description":70,"category":8,"tags":71},"\u002Fposts\u002Fmobile-debugging-today","現在怎麼 debug 手機網頁：從裝一堆工具到幾乎不用裝","對照 2022 年那套 scoop、iTunes、remotedebug-ios-webkit-adapter 的繁瑣設定，整理現在偵錯手機網頁的做法：Android 無線偵錯、iOS 的實際選項、on-device console、Playwright 裝置模擬，以及本機 HTTPS 的處理。",[72,73,74,75,76],"debug","iOS","Android","Chrome DevTools","Playwright",{"path":78,"title":79,"description":80,"category":16,"tags":81},"\u002Fposts\u002Fplaywright-vs-cypress","E2E 測試現在選什麼：Playwright 與 Cypress 的實際差異","對照幾年前用 Cypress 的做法，說明 Playwright 為什麼變成多數新專案的預設選擇：真正的多瀏覽器支援、免費的平行執行、trace viewer 除錯，以及兩者在架構上的根本差異與各自仍然適合的場景。",[76,82,83,84],"Cypress","E2E","測試",{"path":86,"title":87,"description":88,"category":89,"tags":90},"\u002Fposts\u002Fjs-encoding-today","現在處理編碼：TextEncoder、Uint8Array 的 Base64 方法與 URLSearchParams","整理 JavaScript 現代編碼方式，使用 TextEncoder、TextDecoder 與 Uint8Array 處理 UTF-8 與 Base64，並說明 Base64URL、URLSearchParams 與 encodeURIComponent 的差異。","research",[91,92,93,94,95,18],"編碼","TextEncoder","Base64","URLSearchParams","Uint8Array",{"path":97,"title":98,"description":99,"category":16,"tags":100},"\u002Fposts\u002Fdart-sass-migration","Dart Sass 遷移筆記：node-sass 之後怎麼裝 Sass","node-sass 與 LibSass 已停止維護，現在改用 Dart Sass。整理 webpack 與 Vite 的安裝方式，以及為什麼不再像 node-sass 一樣容易遇到 Node.js 版本與原生編譯問題。",[101,102,103],"Sass","Webpack","Vite",{"path":105,"title":106,"description":107,"category":16,"tags":108},"\u002Fposts\u002Fdayjs-vue3-computed","day.js 搭配 Vue 3 的日期格式化","整理 Vue 3 日期格式化的常見做法，使用 day.js 取代舊專案常見的 moment.js，並以 method 或 computed 取代 Vue 2 filter，也補充原生 Intl.DateTimeFormat 的使用方式。",[44,109,110,111],"day.js","日期","Intl",{"path":113,"title":114,"description":115,"category":16,"tags":116},"\u002Fposts\u002Fes-modules-today","ES Module 現在的樣子：Node ESM、exports 與 import maps","整理現代 JavaScript ES Module 的常見用法，包括 Node.js 原生 ESM、package.json exports、瀏覽器 import maps、JSON import attributes，以及 Vite 動態載入的注意事項。",[117,118,119,120,103,18],"ES Module","Node.js","import maps","exports",{"path":122,"title":123,"description":124,"category":89,"tags":125},"\u002Fposts\u002Fstructured-clone","structuredClone()：終於有內建的深層拷貝","整理 JavaScript structuredClone() 的深層拷貝用法，說明 Date、Map、Set、循環參考的處理方式，以及 Function、class 與 Vue reactive Proxy 的注意事項。",[18,126,127,128],"structuredClone","深拷貝","Vue",{"path":130,"title":131,"description":132,"category":16,"tags":133},"\u002Fposts\u002Fdynamic-import","JavaScript 動態載入：import() 與 Vue 非同步元件","整理靜態 import、動態 import() 與 Vue defineAsyncComponent 的使用方式，並簡單補充 Vite import.meta.glob 的用途。",[18,134,135,103,136],"ES Modules","Vue 3","Code Splitting",{"path":138,"title":139,"description":140,"category":16,"tags":141},"\u002Fposts\u002Fscreen-wake-lock-api","Screen Wake Lock API：不用套件也能讓頁面保持亮著","整理 Screen Wake Lock API 的基本用法，說明 HTTPS、頁面可見性、切換分頁後重新取得 wake lock，以及 Vue／Nuxt 計時器使用時的注意事項。",[142,128,143,144,145,146],"Wake Lock","Nuxt4","行動裝置","計時器","Web API",{"path":148,"title":149,"description":150,"category":16,"tags":151},"\u002Fposts\u002Fhtml-meta-boilerplate-today","現在的 HTML meta 標籤怎麼寫","整理現在一個新頁面該有的 meta 標籤骨架——viewport 補上 viewport-fit=cover 因應瀏海與圓角螢幕、color-scheme 讓表單控制項跟著深色模式換色、theme-color 決定行動瀏覽器工具列顏色，以及基本的 SEO 與 Open Graph 標籤。",[152,153,154],"HTML","meta","SEO",{"path":156,"title":157,"description":158,"category":8,"tags":159},"\u002Fposts\u002Fdocker-node-postgres","把 Node + PostgreSQL 的 backend 裝進 Docker：Dockerfile、.dockerignore 與 compose","從「本機裝了就跑」到打包成容器的通用做法——Dockerfile 每一行的理由與 layer cache 為什麼決定了指令順序、.dockerignore 漏掉 node_modules 對原生模組是災難級的問題、容器之間為什麼不能用 localhost 互相找到對方，以及怎麼驗證資料真的寫進了 volume。",[160,161,118,162,163],"Docker","Docker Compose","PostgreSQL","Express",{"path":165,"title":166,"description":167,"category":16,"tags":168},"\u002Fposts\u002Fnuxt-server-proxy","Nuxt 3 \u002F 4 用 Server API 做代理與轉發","整理 Nuxt 3 與 Nuxt 4 使用 server\u002Fapi 轉發第三方 API 的方式，可以避免瀏覽器 CORS 問題、隱藏 API Key，也能在伺服器端加入快取。",[169,143,170,171,172],"Nuxt3","CORS","Nitro","API",{"path":174,"title":175,"description":176,"category":16,"tags":177},"\u002Fposts\u002Fnuxt4-project-setup","Nuxt 4 建立專案：和 Nuxt 3 有哪些不同","整理 Nuxt 4 建立專案的基本流程與目錄結構，並對照 Nuxt 3 的差異，包括 app\u002F 目錄、路徑別名、TypeScript，以及目前常用的 ESLint 與 Tailwind CSS 4 設定。",[143,44,178,179,180],"TypeScript","ESLint","Tailwind CSS 4",{"path":182,"title":183,"description":184,"category":16,"tags":185},"\u002Fposts\u002Fnuxt3-route-middleware","Nuxt 3 \u002F 4 的 Route Middleware：進頁面前先做判斷","整理 Nuxt 3 與 Nuxt 4 的 Route Middleware，用 defineNuxtRouteMiddleware 在進入頁面前檢查登入狀態，搭配 navigateTo 重新導向，並說明一般 middleware、global middleware 與 definePageMeta 的用法。",[169,143,186,187],"Middleware","驗證",{"path":189,"title":190,"description":191,"category":16,"tags":192},"\u002Fposts\u002Fnuxt-route-rules-redirect","Nuxt 3 \u002F 4 的 routeRules：重新導向與頁面設定","整理 Nuxt 3 與 Nuxt 4 的 routeRules 用法，可以依照不同網址設定重新導向、預渲染、SSR 與快取，並簡單對照 Nuxt 2 的舊做法。",[169,143,193,194],"routeRules","重新導向",{"path":196,"title":197,"description":198,"category":16,"tags":199},"\u002Fposts\u002Fios-iframe-google-login-blank-page","iOS iframe 串接 Google 登入出現空白頁：Popup 與頁面回收問題排查紀錄","記錄 iframe 內串接 Google 第三方登入時，部分 iOS 裝置因背景頁面遭到回收並重新載入，導致 Popup 登入流程中斷，以及最後透過 Redirect 分流與登入狀態接回機制處理的過程。",[73,200,201,202,203,204,205,206],"Safari","iframe","Google Login","GIS","OAuth","Popup","Redirect",{"path":208,"title":209,"description":210,"category":16,"tags":211},"\u002Fposts\u002Fios-login-tab-return-behavior","iOS 第三方登入後回不到原頁？分頁開啟關係與回頁行為實測","記錄 iOS Safari 與 Chrome 在多分頁第三方登入流程中，登入頁關閉後不一定回到原頁的實測，以及分頁開啟關係、window.close 與 window.focus 的限制。",[73,200,212,202,204,201,213,214],"Chrome","window.close","window.focus",{"path":216,"title":217,"description":218,"category":16,"tags":219},"\u002Fposts\u002Fgoogle-app-about-blank-placeholder","Google App 內建瀏覽器：about:blank 佔位頁被關閉的排查紀錄","記錄 iOS Google App 內建瀏覽器在非同步開啟外部頁面時，about:blank 佔位分頁被自動關閉，導致第三方登入或外部導轉失敗的排查與解法。",[73,220,221,222,204,223],"Google App","window.open","about:blank","WebView",{"path":225,"title":226,"description":227,"category":228,"tags":229},"\u002Fposts\u002Fclaude-code-codex-cli-init-basics","AI 相關紀錄 v1｜Claude Code \u002F Codex CLI 初始化與基本概念","從 Claude Code 與 Codex CLI 開始，整理 CLI、Coding Agent、Context、\u002Finit 與 CLAUDE.md／AGENTS.md 這幾個一開始最容易搞混的概念，以及在既有專案中的安裝與啟動方式。","ai",[230,231,232,233,234,235],"AI","Claude Code","Codex CLI","Coding Agent","CLAUDE.md","AGENTS.md",{"path":237,"title":238,"description":239,"category":228,"tags":240},"\u002Fposts\u002Fclaude-agents-md-project-guidelines","AI 相關紀錄 v2｜CLAUDE.md \u002F AGENTS.md 怎麼寫？專案規範與實際範例","整理 CLAUDE.md 與 AGENTS.md 的用途、適合記錄的專案規範，以及可直接參考的設定範例。",[230,231,232,234,235],{"path":242,"title":243,"description":244,"category":228,"tags":245},"\u002Fposts\u002Fclaude-codex-config-locations","AI 相關紀錄 v3｜Claude Code `.claude\u002F` \u002F Codex CLI 設定位置：全域與專案設定基本概要介紹","整理 Claude Code 與 Codex CLI 的使用者層級、專案層級設定位置，以及 rules、skills、agents 等目錄的用途。",[230,231,232,246],"設定檔",{"path":248,"title":249,"description":250,"category":228,"tags":251},"\u002Fposts\u002Fclaude-settings-codex-config","AI 相關紀錄 v4｜Claude Code settings \u002F Codex config 基本設定","整理 Claude Code settings.json 與 Codex config.toml 的設定層級、權限控制和常見基本設定。",[230,231,232,252,253],"settings.json","config.toml",{"path":255,"title":256,"description":257,"category":228,"tags":258},"\u002Fposts\u002Fclaude-codex-rules-structure","AI 相關紀錄 v5｜Rules：專案規則多了之後怎麼拆？","整理 Claude Code Rules 與 Codex AGENTS.md 的拆分方式，說明何時該拆、怎麼依目錄管理專案規則。",[230,231,232,259,235],"Rules",{"path":261,"title":262,"description":263,"category":228,"tags":264},"\u002Fposts\u002Fclaude-codex-slash-commands","AI 相關紀錄 v6｜Commands \u002F Slash Commands：CLI 裡的 `\u002F` 指令是什麼？","介紹 Claude Code 與 Codex CLI 的 Slash Commands，整理常用內建指令、自訂指令與 Skills 的差異。",[230,231,232,265],"Slash Commands",{"path":267,"title":268,"description":269,"category":228,"tags":270},"\u002Fposts\u002Fclaude-codex-skills","AI 相關紀錄 v7｜Skills：把重複工作流程整理起來","介紹 Claude Code 與 Codex Skills，整理適合保存成 Skill 的重複流程、基本結構與使用方式。",[230,231,232,271],"Skills",{"path":273,"title":274,"description":275,"category":228,"tags":276},"\u002Fposts\u002Fclaude-codex-agents-subagents","AI 相關紀錄 v8｜Agent \u002F Subagent：什麼時候真的需要分工？","整理 Agent、Subagent 與 Custom Agent 的用途，說明何時適合分工，以及它們和 Rules、Skills 的差異。",[230,231,232,277,278],"Agent","Subagent",{"path":280,"title":281,"description":282,"category":228,"tags":283},"\u002Fposts\u002Fmcp-basics","AI 相關紀錄 v9｜MCP：讓 AI 使用其他工具與資料","介紹 MCP 如何讓 AI 使用外部工具與資料，並以 Pencil MCP 說明實際用途、權限風險及與 Skill 的差異。",[230,284,231,232,285],"MCP","Pencil",{"path":287,"title":288,"description":289,"category":228,"tags":290},"\u002Fposts\u002Fai-coding-agent-concepts-summary","AI 相關紀錄 v10｜基本概念小結論","用一篇快速整理 CLAUDE.md、AGENTS.md、設定檔、Rules、Commands、Skills、Agent 與 MCP 的角色和差異。",[230,231,232,284,271,277],{"path":292,"title":293,"description":294,"category":228,"tags":295},"\u002Fposts\u002Fclaude-code-codex-model-selection","Claude Code 與 Codex：不同模型怎麼選？","比較 Claude Code 與 Codex 不同模型的速度、能力與成本取捨，依工作難度整理實用的選擇方向。",[230,231,296,297],"Codex","模型選擇",{"path":299,"title":300,"description":301,"category":228,"tags":302},"\u002Fposts\u002Fclaude-model-selection","Claude 模型簡單怎麼選？","整理 Claude 各模型在速度、能力與使用情境上的差異，提供從輕量工作到複雜任務的選擇方向。",[230,231,303,297],"Claude",{"path":305,"title":306,"description":307,"category":228,"tags":308},"\u002Fposts\u002Fcodex-model-selection","Codex 模型簡單怎麼選？","整理 Codex 各模型在速度、能力與使用情境上的差異，提供從簡單修改到大型端到端任務的選擇方向。",[230,296,309,297],"OpenAI",{"path":311,"title":312,"description":313,"category":228,"tags":314},"\u002Fposts\u002Fclaude-code-codex-cli-command-cheatsheet","Claude Code \u002F Codex CLI 常用指令速查表","整理 Claude Code 與 Codex CLI 的常用斜線指令、快捷操作與進階功能，方便需要時快速查找。",[230,231,232,265,315],"速查表",{"id":317,"title":217,"body":318,"category":16,"description":218,"draft":928,"extension":929,"image":930,"meta":931,"navigation":415,"path":216,"publishedAt":932,"relatedExperiments":933,"seo":934,"stem":935,"tags":936,"updatedAt":932,"__hash__":937},"posts\u002Fposts\u002F20260825.google-app-about-blank-placeholder.md",{"type":319,"value":320,"toc":921},"minimark",[321,330,333,345,350,353,356,366,373,376,436,439,442,509,512,518,521,525,528,531,537,540,547,550,564,571,574,577,636,639,645,648,654,657,694,697,701,704,711,764,767,828,831,837,840,843,846,852,855,861,864,870,873,889,892,901,904,907,917],[322,323,324,325,329],"p",{},"這次遇到的問題，是在 iOS 的 ",[326,327,328],"strong",{},"Google App 內建瀏覽器","中，第三方登入或外部導轉偶爾會完全沒有反應，最後只能等到流程逾時。",[322,331,332],{},"最後查到的原因不是 API，也不是 Google 登入本身，而是：",[334,335,336],"blockquote",{},[322,337,338],{},[326,339,340,341,344],{},"原本預先開好的 ",[342,343,222],"code",{}," 分頁，在等待伺服器回覆期間，被 Google App 自己關掉了。",[346,347,349],"h2",{"id":348},"為什麼需要先開一個空白分頁","為什麼需要先開一個空白分頁？",[322,351,352],{},"第三方登入或付款流程通常不是點下按鈕後，就立刻知道要前往哪個網址。",[322,354,355],{},"實際流程比較像：",[357,358,364],"pre",{"className":359,"code":361,"language":362,"meta":363},[360],"language-text","使用者點擊按鈕\n↓\n向 Server 取得導轉網址\n↓\n等待回應\n↓\n開啟外部網站\n","text","",[342,365,361],{"__ignoreMap":363},[322,367,368,369,372],{},"但瀏覽器對 ",[342,370,371],{},"window.open()"," 有 Popup Blocker 限制。",[322,374,375],{},"如果等非同步請求完成後才呼叫：",[357,377,381],{"className":378,"code":379,"language":380,"meta":363,"style":363},"language-js shiki shiki-themes github-light github-dark-default","const url = await getRedirectUrl();\n\nwindow.open(url, \"_blank\");\n","js",[342,382,383,410,417],{"__ignoreMap":363},[384,385,388,392,396,399,402,406],"span",{"class":386,"line":387},"line",1,[384,389,391],{"class":390},"sDgYj","const",[384,393,395],{"class":394},"sPgVT"," url",[384,397,398],{"class":390}," =",[384,400,401],{"class":390}," await",[384,403,405],{"class":404},"sVjZL"," getRedirectUrl",[384,407,409],{"class":408},"szmH1","();\n",[384,411,413],{"class":386,"line":412},2,[384,414,416],{"emptyLinePlaceholder":415},true,"\n",[384,418,420,423,426,429,433],{"class":386,"line":419},3,[384,421,422],{"class":408},"window.",[384,424,425],{"class":404},"open",[384,427,428],{"class":408},"(url, ",[384,430,432],{"class":431},"sNbzA","\"_blank\"",[384,434,435],{"class":408},");\n",[322,437,438],{},"新視窗可能已經失去使用者操作帶來的開窗權限，而被瀏覽器阻擋。",[322,440,441],{},"因此原本採用的是「先卡位、後導轉」：",[357,443,445],{"className":378,"code":444,"language":380,"meta":363,"style":363},"const win = window.open(\"about:blank\", \"_blank\");\n\nconst url = await getRedirectUrl();\n\nwin.location.href = url;\n",[342,446,447,474,478,492,497],{"__ignoreMap":363},[384,448,449,451,454,456,459,461,464,467,470,472],{"class":386,"line":387},[384,450,391],{"class":390},[384,452,453],{"class":394}," win",[384,455,398],{"class":390},[384,457,458],{"class":408}," window.",[384,460,425],{"class":404},[384,462,463],{"class":408},"(",[384,465,466],{"class":431},"\"about:blank\"",[384,468,469],{"class":408},", ",[384,471,432],{"class":431},[384,473,435],{"class":408},[384,475,476],{"class":386,"line":412},[384,477,416],{"emptyLinePlaceholder":415},[384,479,480,482,484,486,488,490],{"class":386,"line":419},[384,481,391],{"class":390},[384,483,395],{"class":394},[384,485,398],{"class":390},[384,487,401],{"class":390},[384,489,405],{"class":404},[384,491,409],{"class":408},[384,493,495],{"class":386,"line":494},4,[384,496,416],{"emptyLinePlaceholder":415},[384,498,500,503,506],{"class":386,"line":499},5,[384,501,502],{"class":408},"win.location.href ",[384,504,505],{"class":390},"=",[384,507,508],{"class":408}," url;\n",[322,510,511],{},"流程變成：",[357,513,516],{"className":514,"code":515,"language":362,"meta":363},[360],"使用者點擊\n↓\n先開 about:blank\n↓\n等待 Server\n↓\n取得網址\n↓\n把剛才的分頁導向真正網址\n",[342,517,515],{"__ignoreMap":363},[322,519,520],{},"這個做法在一般 Safari、Chrome 上原本都能正常使用。",[346,522,524],{"id":523},"google-app-的問題aboutblank-自己消失了","Google App 的問題：about:blank 自己消失了",[322,526,527],{},"問題出現在 Google App 的 iOS 內建瀏覽器。",[322,529,530],{},"實測發現：",[357,532,535],{"className":533,"code":534,"language":362,"meta":363},[360],"window.open(\"about:blank\")\n↓\n剛開啟時 window.closed = false\n↓\n等待約 1 秒\n↓\nwindow.closed = true\n",[342,536,534],{"__ignoreMap":363},[322,538,539],{},"也就是：",[334,541,542],{},[322,543,544],{},[326,545,546],{},"分頁確實成功開啟了，但還沒等 Server 回傳網址，就被容器關掉了。",[322,548,549],{},"後續即使再執行：",[357,551,553],{"className":378,"code":552,"language":380,"meta":363,"style":363},"win.location.href = redirectUrl;\n",[342,554,555],{"__ignoreMap":363},[384,556,557,559,561],{"class":386,"line":387},[384,558,502],{"class":408},[384,560,505],{"class":390},[384,562,563],{"class":408}," redirectUrl;\n",[322,565,566,567,570],{},"也已經沒有作用，因為 ",[342,568,569],{},"win"," 指向的視窗早就被關閉。",[346,572,573],{"id":573},"換成真正的佔位頁後就正常",[322,575,576],{},"為了確認問題是不是「跨網域導轉」造成的，當時做了幾組對照測試：",[578,579,580,593],"table",{},[581,582,583],"thead",{},[584,585,586,590],"tr",{},[587,588,589],"th",{},"測試方式",[587,591,592],{},"結果",[594,595,596,605,615,622,629],"tbody",{},[584,597,598,602],{},[599,600,601],"td",{},"直接開啟真正網址",[599,603,604],{},"✅ 正常",[584,606,607,612],{},[599,608,609,611],{},[342,610,222],{}," → 延遲後導轉",[599,613,614],{},"❌ 失敗",[584,616,617,620],{},[599,618,619],{},"同源佔位頁 → 延遲後導轉",[599,621,604],{},[584,623,624,627],{},[599,625,626],{},"同源佔位頁 → Google",[599,628,604],{},[584,630,631,634],{},[599,632,633],{},"同源佔位頁 → 其他外部網站",[599,635,604],{},[322,637,638],{},"最關鍵的差異就是：",[357,640,643],{"className":641,"code":642,"language":362,"meta":363},[360],"about:blank\n→ 會被關閉\n",[342,644,642],{"__ignoreMap":363},[322,646,647],{},"而：",[357,649,652],{"className":650,"code":651,"language":362,"meta":363},[360],"\u002Fredirecting.html\n→ 分頁持續存在\n→ 可以正常導向下一個網址\n",[342,653,651],{"__ignoreMap":363},[322,655,656],{},"因此最後沒有再使用純空白頁，而是改成一個非常簡單的同源佔位頁：",[357,658,662],{"className":659,"code":660,"language":661,"meta":363,"style":363},"language-html shiki shiki-themes github-light github-dark-default","\u003Ch1>處理中\u003C\u002Fh1>\n\u003Cp>請稍候...\u003C\u002Fp>\n","html",[342,663,664,681],{"__ignoreMap":363},[384,665,666,669,673,676,678],{"class":386,"line":387},[384,667,668],{"class":408},"\u003C",[384,670,672],{"class":671},"sXQ1o","h1",[384,674,675],{"class":408},">處理中\u003C\u002F",[384,677,672],{"class":671},[384,679,680],{"class":408},">\n",[384,682,683,685,687,690,692],{"class":386,"line":412},[384,684,668],{"class":408},[384,686,322],{"class":671},[384,688,689],{"class":408},">請稍候...\u003C\u002F",[384,691,322],{"class":671},[384,693,680],{"class":408},[322,695,696],{},"不需要複雜 JavaScript，只要它是一個真正載入完成的頁面即可。",[346,698,700],{"id":699},"只針對-google-app-分流","只針對 Google App 分流",[322,702,703],{},"因為 Safari、Chrome 原本的流程沒有問題，所以最後沒有全面修改。",[322,705,706,707,710],{},"而是透過 User-Agent 中的 ",[342,708,709],{},"GSA\u002F"," 特徵辨識這次遇到的 Google App 環境：",[357,712,716],{"className":713,"code":714,"language":715,"meta":363,"style":363},"language-ts shiki shiki-themes github-light github-dark-default","function isGoogleApp() {\n  return \u002F\\bGSA\\\u002F\u002F.test(navigator.userAgent);\n}\n","ts",[342,717,718,729,759],{"__ignoreMap":363},[384,719,720,723,726],{"class":386,"line":387},[384,721,722],{"class":390},"function",[384,724,725],{"class":404}," isGoogleApp",[384,727,728],{"class":408},"() {\n",[384,730,731,734,737,740,743,747,750,753,756],{"class":386,"line":412},[384,732,733],{"class":390},"  return",[384,735,736],{"class":431}," \u002F",[384,738,739],{"class":390},"\\b",[384,741,742],{"class":431},"GSA",[384,744,746],{"class":745},"sh9l7","\\\u002F",[384,748,749],{"class":431},"\u002F",[384,751,752],{"class":408},".",[384,754,755],{"class":404},"test",[384,757,758],{"class":408},"(navigator.userAgent);\n",[384,760,761],{"class":386,"line":419},[384,762,763],{"class":408},"}\n",[322,765,766],{},"再決定預開哪一種頁面：",[357,768,770],{"className":713,"code":769,"language":715,"meta":363,"style":363},"const placeholderUrl = isGoogleApp()\n  ? \"\u002Fredirecting.html\"\n  : \"about:blank\";\n\nconst win = window.open(placeholderUrl, \"_blank\");\n",[342,771,772,786,794,805,809],{"__ignoreMap":363},[384,773,774,776,779,781,783],{"class":386,"line":387},[384,775,391],{"class":390},[384,777,778],{"class":394}," placeholderUrl",[384,780,398],{"class":390},[384,782,725],{"class":404},[384,784,785],{"class":408},"()\n",[384,787,788,791],{"class":386,"line":412},[384,789,790],{"class":390},"  ?",[384,792,793],{"class":431}," \"\u002Fredirecting.html\"\n",[384,795,796,799,802],{"class":386,"line":419},[384,797,798],{"class":390},"  :",[384,800,801],{"class":431}," \"about:blank\"",[384,803,804],{"class":408},";\n",[384,806,807],{"class":386,"line":494},[384,808,416],{"emptyLinePlaceholder":415},[384,810,811,813,815,817,819,821,824,826],{"class":386,"line":499},[384,812,391],{"class":390},[384,814,453],{"class":394},[384,816,398],{"class":390},[384,818,458],{"class":408},[384,820,425],{"class":404},[384,822,823],{"class":408},"(placeholderUrl, ",[384,825,432],{"class":431},[384,827,435],{"class":408},[322,829,830],{},"概念就是：",[357,832,835],{"className":833,"code":834,"language":362,"meta":363},[360],"Google App\n└─ 開真正的同源佔位頁\n\nSafari \u002F Chrome\n└─ 維持原本 about:blank\n",[342,836,834],{"__ignoreMap":363},[322,838,839],{},"這樣可以把修改範圍控制在實際發生問題的環境，不影響原本正常的瀏覽器。",[346,841,842],{"id":842},"這次踩坑後的心得",[322,844,845],{},"這次最容易誤判的地方，是：",[357,847,850],{"className":848,"code":849,"language":362,"meta":363},[360],"window.open() 有成功\n",[342,851,849],{"__ignoreMap":363},[322,853,854],{},"不代表：",[357,856,859],{"className":857,"code":858,"language":362,"meta":363},[360],"這個 Window 一段時間後還活著\n",[342,860,858],{"__ignoreMap":363},[322,862,863],{},"如果流程是：",[357,865,868],{"className":866,"code":867,"language":362,"meta":363},[360],"先開視窗\n↓\n等待非同步結果\n↓\n再修改 location\n",[342,869,867],{"__ignoreMap":363},[322,871,872],{},"除了要檢查：",[357,874,876],{"className":378,"code":875,"language":380,"meta":363,"style":363},"win !== null\n",[342,877,878],{"__ignoreMap":363},[384,879,880,883,886],{"class":386,"line":387},[384,881,882],{"class":408},"win ",[384,884,885],{"class":390},"!==",[384,887,888],{"class":394}," null\n",[322,890,891],{},"其實還可以注意：",[357,893,895],{"className":378,"code":894,"language":380,"meta":363,"style":363},"win.closed\n",[342,896,897],{"__ignoreMap":363},[384,898,899],{"class":386,"line":387},[384,900,894],{"class":408},[322,902,903],{},"尤其是在 App 內建瀏覽器或 WebView 環境中，行為不一定和 Safari、Chrome 完全相同。",[322,905,906],{},"最後我的處理原則變得很簡單：",[334,908,909],{},[322,910,911],{},[326,912,913,914,916],{},"如果必須預開分頁等待非同步結果，不要假設 ",[342,915,222],{}," 在所有瀏覽器容器裡都會一直存在；必要時使用真正的同源佔位頁。",[918,919,920],"style",{},"html pre.shiki code .sDgYj, html code.shiki .sDgYj{--shiki-default:#D73A49;--shiki-dark:#FF7B72}html pre.shiki code .sPgVT, html code.shiki .sPgVT{--shiki-default:#005CC5;--shiki-dark:#79C0FF}html pre.shiki code .sVjZL, html code.shiki .sVjZL{--shiki-default:#6F42C1;--shiki-dark:#D2A8FF}html pre.shiki code .szmH1, html code.shiki .szmH1{--shiki-default:#24292E;--shiki-dark:#E6EDF3}html pre.shiki code .sNbzA, html code.shiki .sNbzA{--shiki-default:#032F62;--shiki-dark:#A5D6FF}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 .sXQ1o, html code.shiki .sXQ1o{--shiki-default:#22863A;--shiki-dark:#7EE787}html pre.shiki code .sh9l7, html code.shiki .sh9l7{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#7EE787;--shiki-dark-font-weight:bold}",{"title":363,"searchDepth":419,"depth":419,"links":922},[923,924,925,926,927],{"id":348,"depth":412,"text":349},{"id":523,"depth":412,"text":524},{"id":573,"depth":412,"text":573},{"id":699,"depth":412,"text":700},{"id":842,"depth":412,"text":842},false,"md",null,{},"2026-08-25",[],{"title":217,"description":218},"posts\u002F20260825.google-app-about-blank-placeholder",[73,220,221,222,204,223],"n4rLZld8X2TtKKuGnKM_YzsgAJS0E7TphWCwdsm3Bgk",[939,942],{"title":238,"path":237,"stem":940,"description":239,"category":228,"publishedAt":941,"children":-1},"posts\u002F20260902.claude-agents-md-project-guidelines","2026-09-02",{"title":209,"path":208,"stem":943,"description":210,"category":16,"publishedAt":944,"children":-1},"posts\u002F20260725.ios-login-tab-return-behavior","2026-07-25",1791786900795]