[{"data":1,"prerenderedAt":1353},["ShallowReactive",2],{"search-dialog-posts":3,"post-\u002Fposts\u002Fnuxt-server-proxy":316,"post-surround-\u002Fposts\u002Fnuxt-server-proxy":1346},[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":166,"body":318,"category":16,"description":167,"draft":1336,"extension":1337,"image":1338,"meta":1339,"navigation":608,"path":165,"publishedAt":1340,"relatedExperiments":1341,"seo":1342,"stem":1343,"tags":1344,"updatedAt":1340,"__hash__":1345},"posts\u002Fposts\u002F20260619.nuxt-server-proxy.md",{"type":319,"value":320,"toc":1326},"minimark",[321,330,333,336,346,349,354,357,363,366,435,438,444,447,475,478,484,487,490,494,497,503,506,511,518,523,525,531,534,540,547,551,554,557,672,677,717,720,723,728,731,734,737,822,825,831,834,840,843,846,849,912,915,921,924,927,933,936,985,988,994,997,999,1080,1084,1179,1182,1188,1191,1197,1200,1206,1209,1212,1215,1221,1224,1281,1284,1290,1293,1296,1322],[322,323,324,325,329],"p",{},"Nuxt 2 時期常使用 ",[326,327,328],"code",{},"@nuxtjs\u002Fproxy"," 把 Nuxt 當成代理伺服器來解決 CORS。",[322,331,332],{},"到了 Nuxt 3，Nuxt 已經內建 Nitro Server；Nuxt 4 也延續相同架構，所以不一定需要另外安裝 proxy 套件。",[322,334,335],{},"可以簡單理解成：",[337,338,344],"pre",{"className":339,"code":341,"language":342,"meta":343},[340],"language-text","瀏覽器\n↓\n自己的 Nuxt API\n↓\n第三方 API\n","text","",[326,345,341],{"__ignoreMap":343},[322,347,348],{},"前端不直接連第三方服務，而是先請 Nuxt Server 幫忙取得資料。",[350,351,353],"h2",{"id":352},"建立自己的-api","建立自己的 API",[322,355,356],{},"例如建立：",[337,358,361],{"className":359,"code":360,"language":342,"meta":343},[340],"server\u002Fapi\u002Fposts.ts\n",[326,362,360],{"__ignoreMap":343},[322,364,365],{},"內容：",[337,367,371],{"className":368,"code":369,"language":370,"meta":343,"style":343},"language-ts shiki shiki-themes github-light github-dark-default","export default defineEventHandler(async () => {\n  return await $fetch('https:\u002F\u002Fbackend.example.com\u002Fposts')\n})\n","ts",[326,372,373,406,427],{"__ignoreMap":343},[374,375,378,382,385,389,393,396,399,402],"span",{"class":376,"line":377},"line",1,[374,379,381],{"class":380},"sDgYj","export",[374,383,384],{"class":380}," default",[374,386,388],{"class":387},"sVjZL"," defineEventHandler",[374,390,392],{"class":391},"sfpNf","(",[374,394,395],{"class":380},"async",[374,397,398],{"class":391}," () ",[374,400,401],{"class":380},"=>",[374,403,405],{"class":404},"szmH1"," {\n",[374,407,409,412,415,418,420,424],{"class":376,"line":408},2,[374,410,411],{"class":380},"  return",[374,413,414],{"class":380}," await",[374,416,417],{"class":387}," $fetch",[374,419,392],{"class":404},[374,421,423],{"class":422},"sNbzA","'https:\u002F\u002Fbackend.example.com\u002Fposts'",[374,425,426],{"class":404},")\n",[374,428,430,433],{"class":376,"line":429},3,[374,431,432],{"class":404},"}",[374,434,426],{"class":391},[322,436,437],{},"Nuxt 會自動建立：",[337,439,442],{"className":440,"code":441,"language":342,"meta":343},[340],"\u002Fapi\u002Fposts\n",[326,443,441],{"__ignoreMap":343},[322,445,446],{},"前端就可以：",[337,448,450],{"className":368,"code":449,"language":370,"meta":343,"style":343},"const posts = await $fetch('\u002Fapi\u002Fposts')\n",[326,451,452],{"__ignoreMap":343},[374,453,454,457,461,464,466,468,470,473],{"class":376,"line":377},[374,455,456],{"class":380},"const",[374,458,460],{"class":459},"sPgVT"," posts",[374,462,463],{"class":380}," =",[374,465,414],{"class":380},[374,467,417],{"class":387},[374,469,392],{"class":404},[374,471,472],{"class":422},"'\u002Fapi\u002Fposts'",[374,474,426],{"class":404},[322,476,477],{},"流程會變成：",[337,479,482],{"className":480,"code":481,"language":342,"meta":343},[340],"瀏覽器\n↓\n\u002Fapi\u002Fposts\n↓\nNuxt Server\n↓\nhttps:\u002F\u002Fbackend.example.com\u002Fposts\n",[326,483,481],{"__ignoreMap":343},[322,485,486],{},"瀏覽器只需要和自己的網站溝通。",[322,488,489],{},"第三方 API 的請求則由伺服器發出，因此不會受到瀏覽器 CORS 限制。",[350,491,493],{"id":492},"serverapi-和-serverroutes-的差別","server\u002Fapi 和 server\u002Froutes 的差別",[322,495,496],{},"Nuxt 3 與 Nuxt 4 都有：",[337,498,501],{"className":499,"code":500,"language":342,"meta":343},[340],"server\u002F\n├── api\u002F\n└── routes\u002F\n",[326,502,500],{"__ignoreMap":343},[322,504,505],{},"放在：",[337,507,509],{"className":508,"code":360,"language":342,"meta":343},[340],[326,510,360],{"__ignoreMap":343},[322,512,513,514,517],{},"網址會自動加上 ",[326,515,516],{},"\u002Fapi","：",[337,519,521],{"className":520,"code":441,"language":342,"meta":343},[340],[326,522,441],{"__ignoreMap":343},[322,524,505],{},[337,526,529],{"className":527,"code":528,"language":342,"meta":343},[340],"server\u002Froutes\u002Fposts.ts\n",[326,530,528],{"__ignoreMap":343},[322,532,533],{},"則會直接變成：",[337,535,538],{"className":536,"code":537,"language":342,"meta":343},[340],"\u002Fposts\n",[326,539,537],{"__ignoreMap":343},[322,541,542,543,546],{},"一般自己提供 API 給前端呼叫時，使用 ",[326,544,545],{},"server\u002Fapi\u002F"," 就可以。",[350,548,550],{"id":549},"隱藏-api-key","隱藏 API Key",[322,552,553],{},"如果第三方服務需要 API Key，不應該把金鑰放在前端。",[322,555,556],{},"例如：",[337,558,560],{"className":368,"code":559,"language":370,"meta":343,"style":343},"export default defineEventHandler(async (event) => {\n  const config = useRuntimeConfig(event)\n\n  return await $fetch('https:\u002F\u002Fbackend.example.com\u002Fposts', {\n    headers: {\n      Authorization: `Bearer ${config.apiKey}`\n    }\n  })\n})\n",[326,561,562,588,604,610,626,632,653,659,665],{"__ignoreMap":343},[374,563,564,566,568,570,572,574,577,581,584,586],{"class":376,"line":377},[374,565,381],{"class":380},[374,567,384],{"class":380},[374,569,388],{"class":387},[374,571,392],{"class":391},[374,573,395],{"class":380},[374,575,576],{"class":391}," (",[374,578,580],{"class":579},"sjlk-","event",[374,582,583],{"class":391},") ",[374,585,401],{"class":380},[374,587,405],{"class":404},[374,589,590,593,596,598,601],{"class":376,"line":408},[374,591,592],{"class":380},"  const",[374,594,595],{"class":459}," config",[374,597,463],{"class":380},[374,599,600],{"class":387}," useRuntimeConfig",[374,602,603],{"class":404},"(event)\n",[374,605,606],{"class":376,"line":429},[374,607,609],{"emptyLinePlaceholder":608},true,"\n",[374,611,613,615,617,619,621,623],{"class":376,"line":612},4,[374,614,411],{"class":380},[374,616,414],{"class":380},[374,618,417],{"class":387},[374,620,392],{"class":404},[374,622,423],{"class":422},[374,624,625],{"class":404},", {\n",[374,627,629],{"class":376,"line":628},5,[374,630,631],{"class":404},"    headers: {\n",[374,633,635,638,641,644,647,650],{"class":376,"line":634},6,[374,636,637],{"class":404},"      Authorization: ",[374,639,640],{"class":422},"`Bearer ${",[374,642,643],{"class":404},"config",[374,645,646],{"class":422},".",[374,648,649],{"class":404},"apiKey",[374,651,652],{"class":422},"}`\n",[374,654,656],{"class":376,"line":655},7,[374,657,658],{"class":404},"    }\n",[374,660,662],{"class":376,"line":661},8,[374,663,664],{"class":404},"  })\n",[374,666,668,670],{"class":376,"line":667},9,[374,669,432],{"class":404},[374,671,426],{"class":391},[322,673,674,517],{},[326,675,676],{},"nuxt.config.ts",[337,678,680],{"className":368,"code":679,"language":370,"meta":343,"style":343},"export default defineNuxtConfig({\n  runtimeConfig: {\n    apiKey: ''\n  }\n})\n",[326,681,682,694,699,707,712],{"__ignoreMap":343},[374,683,684,686,688,691],{"class":376,"line":377},[374,685,381],{"class":380},[374,687,384],{"class":380},[374,689,690],{"class":387}," defineNuxtConfig",[374,692,693],{"class":391},"({\n",[374,695,696],{"class":376,"line":408},[374,697,698],{"class":404},"  runtimeConfig: {\n",[374,700,701,704],{"class":376,"line":429},[374,702,703],{"class":404},"    apiKey: ",[374,705,706],{"class":422},"''\n",[374,708,709],{"class":376,"line":612},[374,710,711],{"class":404},"  }\n",[374,713,714],{"class":376,"line":628},[374,715,716],{"class":391},"})\n",[322,718,719],{},"真正部署時再透過環境變數提供金鑰。",[322,721,722],{},"這樣瀏覽器只會看到：",[337,724,726],{"className":725,"code":441,"language":342,"meta":343},[340],[326,727,441],{"__ignoreMap":343},[322,729,730],{},"不會拿到真正的 API Key。",[350,732,733],{"id":733},"可以順便加快取",[322,735,736],{},"如果第三方 API 不需要每次都重新取得，也可以在 Server API 加上快取：",[337,738,740],{"className":368,"code":739,"language":370,"meta":343,"style":343},"export default defineCachedEventHandler(\n  async () => {\n    return await $fetch(\n      'https:\u002F\u002Fbackend.example.com\u002Fposts'\n    )\n  },\n  {\n    maxAge: 60 * 10\n  }\n)\n",[326,741,742,754,765,776,781,786,794,799,813,817],{"__ignoreMap":343},[374,743,744,746,748,751],{"class":376,"line":377},[374,745,381],{"class":380},[374,747,384],{"class":380},[374,749,750],{"class":387}," defineCachedEventHandler",[374,752,753],{"class":391},"(\n",[374,755,756,759,761,763],{"class":376,"line":408},[374,757,758],{"class":380},"  async",[374,760,398],{"class":391},[374,762,401],{"class":380},[374,764,405],{"class":404},[374,766,767,770,772,774],{"class":376,"line":429},[374,768,769],{"class":380},"    return",[374,771,414],{"class":380},[374,773,417],{"class":387},[374,775,753],{"class":404},[374,777,778],{"class":376,"line":612},[374,779,780],{"class":422},"      'https:\u002F\u002Fbackend.example.com\u002Fposts'\n",[374,782,783],{"class":376,"line":628},[374,784,785],{"class":404},"    )\n",[374,787,788,791],{"class":376,"line":634},[374,789,790],{"class":404},"  }",[374,792,793],{"class":391},",\n",[374,795,796],{"class":376,"line":655},[374,797,798],{"class":391},"  {\n",[374,800,801,804,807,810],{"class":376,"line":661},[374,802,803],{"class":404},"    maxAge: ",[374,805,806],{"class":459},"60",[374,808,809],{"class":380}," *",[374,811,812],{"class":459}," 10\n",[374,814,815],{"class":376,"line":667},[374,816,711],{"class":391},[374,818,820],{"class":376,"line":819},10,[374,821,426],{"class":391},[322,823,824],{},"代表：",[337,826,829],{"className":827,"code":828,"language":342,"meta":343},[340],"快取 10 分鐘\n",[326,830,828],{"__ignoreMap":343},[322,832,833],{},"這種方式很適合：",[337,835,838],{"className":836,"code":837,"language":342,"meta":343},[340],"天氣資料\n公開資訊\n新聞列表\n第三方 API\n更新頻率不高的資料\n",[326,839,837],{"__ignoreMap":343},[322,841,842],{},"可以減少重複呼叫上游 API。",[350,844,845],{"id":845},"不要做成任意網址代理",[322,847,848],{},"不要寫成：",[337,850,852],{"className":368,"code":851,"language":370,"meta":343,"style":343},"export default defineEventHandler(async (event) => {\n  const targetUrl = getQuery(event).url\n\n  return await $fetch(targetUrl)\n})\n",[326,853,854,876,891,895,906],{"__ignoreMap":343},[374,855,856,858,860,862,864,866,868,870,872,874],{"class":376,"line":377},[374,857,381],{"class":380},[374,859,384],{"class":380},[374,861,388],{"class":387},[374,863,392],{"class":391},[374,865,395],{"class":380},[374,867,576],{"class":391},[374,869,580],{"class":579},[374,871,583],{"class":391},[374,873,401],{"class":380},[374,875,405],{"class":404},[374,877,878,880,883,885,888],{"class":376,"line":408},[374,879,592],{"class":380},[374,881,882],{"class":459}," targetUrl",[374,884,463],{"class":380},[374,886,887],{"class":387}," getQuery",[374,889,890],{"class":404},"(event).url\n",[374,892,893],{"class":376,"line":429},[374,894,609],{"emptyLinePlaceholder":608},[374,896,897,899,901,903],{"class":376,"line":612},[374,898,411],{"class":380},[374,900,414],{"class":380},[374,902,417],{"class":387},[374,904,905],{"class":404},"(targetUrl)\n",[374,907,908,910],{"class":376,"line":628},[374,909,432],{"class":404},[374,911,426],{"class":391},[322,913,914],{},"例如讓使用者可以：",[337,916,919],{"className":917,"code":918,"language":342,"meta":343},[340],"\u002Fapi\u002Fproxy?url=https:\u002F\u002F任意網址\n",[326,920,918],{"__ignoreMap":343},[322,922,923],{},"這等於讓任何人都能利用你的伺服器去請求其他網站。",[322,925,926],{},"可能造成：",[337,928,931],{"className":929,"code":930,"language":342,"meta":343},[340],"被拿來當免費代理\n浪費流量\n攻擊其他服務\n存取原本不該公開的內部網址\n",[326,932,930],{"__ignoreMap":343},[322,934,935],{},"比較安全的方式是：",[337,937,939],{"className":368,"code":938,"language":370,"meta":343,"style":343},"export default defineEventHandler(async () => {\n  return await $fetch(\n    'https:\u002F\u002Fbackend.example.com\u002Fposts'\n  )\n})\n",[326,940,941,959,969,974,979],{"__ignoreMap":343},[374,942,943,945,947,949,951,953,955,957],{"class":376,"line":377},[374,944,381],{"class":380},[374,946,384],{"class":380},[374,948,388],{"class":387},[374,950,392],{"class":391},[374,952,395],{"class":380},[374,954,398],{"class":391},[374,956,401],{"class":380},[374,958,405],{"class":404},[374,960,961,963,965,967],{"class":376,"line":408},[374,962,411],{"class":380},[374,964,414],{"class":380},[374,966,417],{"class":387},[374,968,753],{"class":404},[374,970,971],{"class":376,"line":429},[374,972,973],{"class":422},"    'https:\u002F\u002Fbackend.example.com\u002Fposts'\n",[374,975,976],{"class":376,"line":612},[374,977,978],{"class":404},"  )\n",[374,980,981,983],{"class":376,"line":628},[374,982,432],{"class":404},[374,984,426],{"class":391},[322,986,987],{},"也就是：",[989,990,991],"blockquote",{},[322,992,993],{},"Nuxt Server 要連哪個 API，由你自己決定，不讓使用者直接傳完整網址。",[322,995,996],{},"如果真的需要動態參數，可以只接受明確的 ID 或固定範圍。",[322,998,556],{},[337,1000,1002],{"className":368,"code":1001,"language":370,"meta":343,"style":343},"export default defineEventHandler(async (event) => {\n  const id = getRouterParam(event, 'id')\n\n  return await $fetch(\n    `https:\u002F\u002Fbackend.example.com\u002Fposts\u002F${id}`\n  )\n})\n",[326,1003,1004,1026,1046,1050,1060,1070,1074],{"__ignoreMap":343},[374,1005,1006,1008,1010,1012,1014,1016,1018,1020,1022,1024],{"class":376,"line":377},[374,1007,381],{"class":380},[374,1009,384],{"class":380},[374,1011,388],{"class":387},[374,1013,392],{"class":391},[374,1015,395],{"class":380},[374,1017,576],{"class":391},[374,1019,580],{"class":579},[374,1021,583],{"class":391},[374,1023,401],{"class":380},[374,1025,405],{"class":404},[374,1027,1028,1030,1033,1035,1038,1041,1044],{"class":376,"line":408},[374,1029,592],{"class":380},[374,1031,1032],{"class":459}," id",[374,1034,463],{"class":380},[374,1036,1037],{"class":387}," getRouterParam",[374,1039,1040],{"class":404},"(event, ",[374,1042,1043],{"class":422},"'id'",[374,1045,426],{"class":404},[374,1047,1048],{"class":376,"line":429},[374,1049,609],{"emptyLinePlaceholder":608},[374,1051,1052,1054,1056,1058],{"class":376,"line":612},[374,1053,411],{"class":380},[374,1055,414],{"class":380},[374,1057,417],{"class":387},[374,1059,753],{"class":404},[374,1061,1062,1065,1068],{"class":376,"line":628},[374,1063,1064],{"class":422},"    `https:\u002F\u002Fbackend.example.com\u002Fposts\u002F${",[374,1066,1067],{"class":404},"id",[374,1069,652],{"class":422},[374,1071,1072],{"class":376,"line":634},[374,1073,978],{"class":404},[374,1075,1076,1078],{"class":376,"line":655},[374,1077,432],{"class":404},[374,1079,426],{"class":391},[350,1081,1083],{"id":1082},"nuxt-234-差在哪","Nuxt 2、3、4 差在哪？",[1085,1086,1087,1105],"table",{},[1088,1089,1090],"thead",{},[1091,1092,1093,1096,1099,1102],"tr",{},[1094,1095],"th",{},[1094,1097,1098],{},"Nuxt 2",[1094,1100,1101],{},"Nuxt 3",[1094,1103,1104],{},"Nuxt 4",[1106,1107,1108,1122,1136,1151,1167],"tbody",{},[1091,1109,1110,1114,1117,1120],{},[1111,1112,1113],"td",{},"Server API",[1111,1115,1116],{},"需要另外處理",[1111,1118,1119],{},"Nitro 內建",[1111,1121,1119],{},[1091,1123,1124,1128,1131,1134],{},[1111,1125,1126],{},[326,1127,545],{},[1111,1129,1130],{},"沒有現在這套方式",[1111,1132,1133],{},"支援",[1111,1135,1133],{},[1091,1137,1138,1141,1146,1149],{},[1111,1139,1140],{},"Proxy 套件",[1111,1142,1143,1144],{},"常用 ",[326,1145,328],{},[1111,1147,1148],{},"通常不需要",[1111,1150,1148],{},[1091,1152,1153,1155,1158,1163],{},[1111,1154,550],{},[1111,1156,1157],{},"另外處理",[1111,1159,1160],{},[326,1161,1162],{},"runtimeConfig",[1111,1164,1165],{},[326,1166,1162],{},[1091,1168,1169,1172,1174,1177],{},[1111,1170,1171],{},"API 快取",[1111,1173,1157],{},[1111,1175,1176],{},"Nitro Cache",[1111,1178,1176],{},[322,1180,1181],{},"所以從 Nuxt 3 升到 Nuxt 4：",[337,1183,1186],{"className":1184,"code":1185,"language":342,"meta":343},[340],"server\u002Fapi\u002F 的基本使用方式沒有太大改變。\n",[326,1187,1185],{"__ignoreMap":343},[322,1189,1190],{},"Nuxt 4 最大的目錄變化是在前端程式搬進：",[337,1192,1195],{"className":1193,"code":1194,"language":342,"meta":343},[340],"app\u002F\n",[326,1196,1194],{"__ignoreMap":343},[322,1198,1199],{},"而：",[337,1201,1204],{"className":1202,"code":1203,"language":342,"meta":343},[340],"server\u002F\n",[326,1205,1203],{"__ignoreMap":343},[322,1207,1208],{},"仍然留在專案根目錄。",[350,1210,1211],{"id":1211},"小結",[322,1213,1214],{},"如果前端需要呼叫第三方 API，可以考慮：",[337,1216,1219],{"className":1217,"code":1218,"language":342,"meta":343},[340],"前端\n↓\nNuxt \u002Fapi\n↓\n第三方 API\n",[326,1220,1218],{"__ignoreMap":343},[322,1222,1223],{},"最基本的寫法：",[337,1225,1227],{"className":368,"code":1226,"language":370,"meta":343,"style":343},"\u002F\u002F server\u002Fapi\u002Fposts.ts\n\nexport default defineEventHandler(async () => {\n  return await $fetch(\n    'https:\u002F\u002Fbackend.example.com\u002Fposts'\n  )\n})\n",[326,1228,1229,1235,1239,1257,1267,1271,1275],{"__ignoreMap":343},[374,1230,1231],{"class":376,"line":377},[374,1232,1234],{"class":1233},"sPyeA","\u002F\u002F server\u002Fapi\u002Fposts.ts\n",[374,1236,1237],{"class":376,"line":408},[374,1238,609],{"emptyLinePlaceholder":608},[374,1240,1241,1243,1245,1247,1249,1251,1253,1255],{"class":376,"line":429},[374,1242,381],{"class":380},[374,1244,384],{"class":380},[374,1246,388],{"class":387},[374,1248,392],{"class":391},[374,1250,395],{"class":380},[374,1252,398],{"class":391},[374,1254,401],{"class":380},[374,1256,405],{"class":404},[374,1258,1259,1261,1263,1265],{"class":376,"line":612},[374,1260,411],{"class":380},[374,1262,414],{"class":380},[374,1264,417],{"class":387},[374,1266,753],{"class":404},[374,1268,1269],{"class":376,"line":628},[374,1270,973],{"class":422},[374,1272,1273],{"class":376,"line":634},[374,1274,978],{"class":404},[374,1276,1277,1279],{"class":376,"line":655},[374,1278,432],{"class":404},[374,1280,426],{"class":391},[322,1282,1283],{},"這樣可以順便處理：",[337,1285,1288],{"className":1286,"code":1287,"language":342,"meta":343},[340],"CORS\nAPI Key\n快取\n資料整理\n",[326,1289,1287],{"__ignoreMap":343},[322,1291,1292],{},"Nuxt 3 與 Nuxt 4 都可以使用相同做法。",[350,1294,1295],{"id":1295},"參考資料",[1297,1298,1299,1309,1316],"ul",{},[1300,1301,1302],"li",{},[1303,1304,1308],"a",{"href":1305,"rel":1306},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Fserver",[1307],"nofollow","Nuxt 4：server 目錄",[1300,1310,1311],{},[1303,1312,1315],{"href":1313,"rel":1314},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002F3.x\u002Fdirectory-structure\u002Fserver",[1307],"Nuxt 3：server 目錄",[1300,1317,1318],{},[1303,1319,171],{"href":1320,"rel":1321},"https:\u002F\u002Fnitro.build\u002F",[1307],[1323,1324,1325],"style",{},"html pre.shiki code .sDgYj, html code.shiki .sDgYj{--shiki-default:#D73A49;--shiki-dark:#FF7B72}html pre.shiki code .sVjZL, html code.shiki .sVjZL{--shiki-default:#6F42C1;--shiki-dark:#D2A8FF}html pre.shiki code .sfpNf, html code.shiki .sfpNf{--shiki-default:#24292E;--shiki-dark:#FFA657}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 .sPgVT, html code.shiki .sPgVT{--shiki-default:#005CC5;--shiki-dark:#79C0FF}html pre.shiki code .sjlk-, html code.shiki .sjlk-{--shiki-default:#E36209;--shiki-dark:#FFA657}html pre.shiki code .sPyeA, html code.shiki .sPyeA{--shiki-default:#6A737D;--shiki-dark:#8B949E}",{"title":343,"searchDepth":429,"depth":429,"links":1327},[1328,1329,1330,1331,1332,1333,1334,1335],{"id":352,"depth":408,"text":353},{"id":492,"depth":408,"text":493},{"id":549,"depth":408,"text":550},{"id":733,"depth":408,"text":733},{"id":845,"depth":408,"text":845},{"id":1082,"depth":408,"text":1083},{"id":1211,"depth":408,"text":1211},{"id":1295,"depth":408,"text":1295},false,"md",null,{},"2026-06-19",[],{"title":166,"description":167},"posts\u002F20260619.nuxt-server-proxy",[169,143,170,171,172],"W2KY_w2ETabEL_1jqMkiu3lQXNvaaAdACjfGMEvUUGk",[1347,1350],{"title":175,"path":174,"stem":1348,"description":176,"category":16,"publishedAt":1349,"children":-1},"posts\u002F20260620.nuxt4-project-setup","2026-06-20",{"title":139,"path":138,"stem":1351,"description":140,"category":16,"publishedAt":1352,"children":-1},"posts\u002F20260516.screen-wake-lock-api","2026-06-16",1791786901048]