[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-categories":3,"blog-category-nuxt":78},[4,10,14,19,24,30,36,40,45,51,55,58,62,68,73],{"id":5,"databaseId":6,"name":7,"slug":8,"count":9},"dGVybTo1NQ==",55,"AI","ai",2,{"id":11,"databaseId":12,"name":13,"slug":13,"count":9},"dGVybTo2MQ==",61,"backend",{"id":15,"databaseId":16,"name":17,"slug":18,"count":16},"dGVybToyMA==",20,"Devops","devops",{"id":20,"databaseId":21,"name":22,"slug":22,"count":23},"dGVybToxNQ==",15,"frontend",25,{"id":25,"databaseId":26,"name":27,"slug":28,"count":29},"dGVybTo2MA==",60,"GAS","gas",1,{"id":31,"databaseId":32,"name":33,"slug":34,"count":35},"dGVybTo5",9,"HTML","html",4,{"id":37,"databaseId":38,"name":39,"slug":39,"count":29},"dGVybToyMg==",22,"mac",{"id":41,"databaseId":42,"name":43,"slug":43,"count":44},"dGVybTo2",6,"node",18,{"id":46,"databaseId":47,"name":48,"slug":49,"count":50},"dGVybTo2OQ==",69,"Nuxt","nuxt",3,{"id":52,"databaseId":53,"name":54,"slug":54,"count":50},"dGVybTo1NA==",54,"office",{"id":56,"databaseId":9,"name":57,"slug":57,"count":21},"dGVybToy","omekas",{"id":59,"databaseId":60,"name":61,"slug":61,"count":16},"dGVybToxNg==",16,"vue",{"id":63,"databaseId":64,"name":65,"slug":66,"count":67},"dGVybTo1",5,"WordPress","wordpress",13,{"id":69,"databaseId":29,"name":70,"slug":71,"count":72},"dGVybTox","無","uncategorized",null,{"id":74,"databaseId":75,"name":76,"slug":77,"count":9},"dGVybTo3MA==",70,"生活","life",[79,94,107],{"id":80,"slug":81,"title":82,"description":83,"content":84,"date":85,"image":86,"imageAlt":82,"author":87,"categories":90,"path":93,"minRead":29},3818,"個人網站改版建置心得-headless-wordpress-nuxt","個人網站改版建置心得 Headless WordPress + Nuxt","一直聽過 WordPress 的無頭模式(Headless)架構，最近心一狠就把自己的網站轉移成無頭模式架構。 […]","\u003Cp>一直聽過 WordPress 的無頭模式(Headless)架構，最近心一狠就把自己的網站轉移成無頭模式架構。\u003C\u002Fp>\n\u003Cp>理由有幾點：\u003C\u002Fp>\n\u003Col>\n\u003Cli>想要更有彈性的在前台開發\u003C\u002Fli>\n\u003Cli>在前台實作一些想法\u003C\u002Fli>\n\u003Cli>體驗看看無頭模式的坑(?\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>記錄一下目前版本做了哪些事情\u003C\u002Fp>\n\u003Ch1>轉換流程\u003C\u002Fh1>\n\u003Ch2>1. 決定架構\u003C\u002Fh2>\n\u003Cp>我決定了前台採用 Nuxt ，一方面我是走 Vue 生態的開發者，二來需要有 SSR 來維持原本的 SEO 效果。\u003C\u002Fp>\n\u003Cp>簡單來說就是 Nuxt + WordPress\u003C\u002Fp>\n\u003Ch2>2. 開啟 WordPress 無頭模式\u003C\u002Fh2>\n\u003Cp>翻了一些文章，多半會推薦無頭模式要採用 Graphql ，主要是為了提高查詢效率，因此先安裝了 wpgraphql ，讓 WordPress 的 RestFul API  支援 Graphql。\u003C\u002Fp>\n\u003Ch2>3. 建置新的前台\u003C\u002Fh2>\n\u003Cp>用 Nuxt 建置了新的前台，目前版本只是先提供最基礎的內容：首頁、介紹、文章等，後續還有一些想法慢慢更新。這時實作串接原網站的 API，就能進行測試了！\u003C\u002Fp>\n\u003Cp>記得要開啟 SSR 模式，這部分除錯比較麻煩一些。\u003C\u002Fp>\n\u003Cp>另外也要處理 CORS 問題，這部分解完後，整個架構雛形就完成了。\u003C\u002Fp>\n\u003Ch2>4. 網域轉換 + SEO\u003C\u002Fh2>\n\u003Cp>將原本的 WordPress 轉換到另一個網域，並且將新前台掛在原本的網域。\u003C\u002Fp>\n\u003Cp>由於新架構的 route 已經改變，自然就會出現文章路徑的轉址問題，我另外寫了一個轉換器和對應表來處理 301 轉址。\u003C\u002Fp>\n\u003Cp>而 SEO 就要在 Nuxt 上全部處理了。\u003C\u002Fp>\n\u003Ch2>5. 關閉 WordPress 的前台模式\u003C\u002Fh2>\n\u003Cp>說是關閉，我採用的做法是強制轉址到前台網域billxu.net 啦.\u003C\u002Fp>\n\u003Cp>這些步驟做完，也差不多把整個架構調完了！\u003C\u002Fp>\n\u003Ch1>簡短心得\u003C\u002Fh1>\n\u003Cp>轉換完後有種舒爽的感覺，想到不用受到原本頁面架構的限制就覺得有種解放感！不過什麼都得重新來過，像是文章排版、資料解析等等。最近還在除錯，之後想在網站中放一些小遊戲之類的(? 到時候就再分享了XD\u003C\u002Fp>\n","2026-01-18T15:29:46","",{"name":88,"link":89},"Chih Fan","http:\u002F\u002Fpost.billxu.net",[91,92],{"name":48,"slug":49},{"name":65,"slug":66},"\u002Fblog\u002F個人網站改版建置心得-headless-wordpress-nuxt",{"id":95,"slug":96,"title":97,"description":98,"content":99,"date":100,"image":101,"imageAlt":97,"author":102,"categories":103,"path":106,"minRead":9},3786,"理解與前端實作-recaptcha-v3","理解與前端實作 reCAPTCHA v3","理解與實作 recaptcha Google reCAPTCHA 是一種常見的機器人驗證技術，用來區分人類使用 […]","\u003Ch1>理解與實作 recaptcha\u003C\u002Fh1>\n\u003Cp>Google reCAPTCHA  是一種常見的機器人驗證技術，用來區分人類使用者和自動化程式。現在主流有 v2 和 v3 兩種版本，是擁有免費額度的付費服務。\u003Cbr \u002F>\n先前都只有透過打包好的 WordPress 外掛來實作，這次有機會在Nuxt3的前端專案中自己寫，就趁機記錄一下自己的理解與過程。\u003C\u002Fp>\n\u003Cp>本次使用的是 reCAPTCHA v3 ，特點在於不用跟介面互動就能取得辨識結果，好處就是網站對使用者的驗證只會在背景處理，不影響使用體驗。\u003C\u002Fp>\n\u003Cp>reCAPTCHA v3的技術原理大概可以這樣理解：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>行為分析：評估使用者與頁面的互動方式，如滑鼠移動模式、點擊方式和速度。\u003C\u002Fli>\n\u003Cli>瀏覽器指紋識別：收集瀏覽器和設備資訊，如作業系統、瀏覽器類型、螢幕解析度等。\u003C\u002Fli>\n\u003Cli>Cookie 和歷史數據：分析用戶之前與 Google 服務的互動歷史。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>實作邏輯的話，其實就是跟 reCAPTCHA 服務取用資料這樣。\u003C\u002Fp>\n\u003Ch3>前置：取得金鑰\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>網站金鑰 (Site Key)：公開的，嵌入在網頁前端代碼中\u003C\u002Fli>\n\u003Cli>密鑰 (Secret Key)：私密的，只在伺服器端使用\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>實作流程\u003C\u002Fh3>\n\u003Cp>以下是我對  reCAPTCHA 溝通的理解：\u003C\u002Fp>\n\u003Ch4>1. 前端向 Google reCAPTCHA 請求 token\u003C\u002Fh4>\n\u003Cp>首先註冊並申請 Google reCAPTCHA 服務。\u003Cbr \u002F>\n網站前端載入 reCAPTCHA SDK，當使用者執行需驗證的操作時，呼叫grecaptcha.execute()， reCAPTCHA 服務會分析使用者行為後回傳 token。\u003C\u002Fp>\n\u003Ch4>2. 前端向網站伺服器提交請求\u003C\u002Fh4>\n\u003Cp>前端將 reCAPTCHA token 連同表單數據或 API 請求一起發送給後端。\u003C\u002Fp>\n\u003Ch4>3. 伺服器向 Google reCAPTCHA 驗證 token\u003C\u002Fh4>\n\u003Cp>後端使用密鑰(secret key)向 Google reCAPTCHA API 發送驗證請求。\u003C\u002Fp>\n\u003Ch4>4. Google reCAPTCHA 返回驗證結果\u003C\u002Fh4>\n\u003Cp>JSON 格式的回應包含:\u003Cbr \u002F>\nsuccess: 驗證是否成功(true\u002Ffalse)\u003Cbr \u002F>\nscore: 闕值，人類可能性分數(0.0-1.0)\u003Cbr \u002F>\naction: 觸發驗證的行為名稱\u003Cbr \u002F>\nchallenge_ts: 時間戳記\u003Cbr \u002F>\nhostname: 網站域名\u003C\u002Fp>\n\u003Ch3>閾值判斷：\u003C\u002Fh3>\n\u003Cp>我這次使用的 reCAPTCHA v3 會回傳 0.0 到 1.0 之間的分數，此分數表示使用者行為的「人類可能性」，管理者可自行決定接受的閾值（例如 0.5）高於閾值就允許後續操作，低於則可能需要額外驗證或拒絕請求。\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fwp-content\u002Fuploads\u002F2025\u002F04\u002Fdiagram.svg\">\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2025%2F04%2Fdiagram.svg\" alt=\"\" \u002F>\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>實作紀錄：\u003C\u002Fh2>\n\u003Cp>我使用 vue-recaptcha-v3 ，自行建立 plugin 管理與啟動 reCAPTCHA，因為只有前端實作，所以很快就處理好了。\u003C\u002Fp>\n\u003Cp>recaptcha.client.js\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { VueReCaptcha } from &#039;vue-recaptcha-v3&#039;\n\nexport default defineNuxtPlugin(async (nuxtApp) =&gt; {\n  const config = useRuntimeConfig()\n\n  \u002F\u002F 確保 siteKey 存在並輸出調試信息\n  console.log(&#039;Initializing reCAPTCHA with config:&#039;, {\n    siteKey: config.public.recaptchaSiteKey,\n    isDevelopment: process.env.NODE_ENV === &#039;development&#039;\n  })\n\n  if (!config.public.recaptchaSiteKey) {\n    console.error(&#039;reCAPTCHA site key is missing&#039;)\n    return\n  }\n\n  try {\n    await nuxtApp.vueApp.use(VueReCaptcha, {\n      siteKey: config.public.recaptchaSiteKey,\n      loaderOptions: {\n        useRecaptchaNet: true, \u002F\u002F 從 recaptcha.net 載入 script\n        autoHideBadge: false,\u002F\u002F 隱藏標籤\n        explicitRenderParameters: {\n          badge: &#039;bottomright&#039;,\n          size: &#039;invisible&#039;\n        }\n      }\n    })\n\n    \u002F\u002F 驗證 reCAPTCHA 是否正確載入\n    const recaptcha = await window.grecaptcha\n    if (!recaptcha) {\n      throw new Error(&#039;reCAPTCHA not loaded properly&#039;)\n    }\n\n    console.log(&#039;reCAPTCHA plugin initialized successfully&#039;)\n  } catch (error) {\n    console.error(&#039;Failed to initialize reCAPTCHA:&#039;, error)\n  }\n}) \u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後在 .env 和 nuxt.config 放好 API_KEY\u003C\u002Fp>\n\u003Cp>例如在登入時就宣告上述的流程即可\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { useReCaptcha } from &#039;vue-recaptcha-v3&#039;\nconst { executeRecaptcha, recaptchaLoaded } = useReCaptcha()\n\nconst handleLogin = async (e) =&gt; {\n\n\u002F\u002F 執行 reCAPTCHA 驗證\nconsole.log(&#039;開始執行 reCAPTCHA 驗證&#039;)\nconst recaptchaToken = await executeRecaptcha(&#039;login&#039;)\nconsole.log(&#039;recaptchaToken:&#039;, recaptchaToken)\n\n\u002F\u002F 發送登入請求\nconst response = await $fetch(`\u002Fapi\u002Flogin`, {\n    method: &#039;POST&#039;,\n        body: {\n        account: account.value,\n        password: password.value,\n        recaptchaToken: recaptchaToken\n        }\n    })\nif (response) {\n\u002F\u002F後端回傳闕值做後續處理\n}\n\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>後端就要另外寫一隻配合密鑰的請求去詢問結果。\u003C\u002Fp>\n\u003Cp>成功載入的話，網站會出現 reCAPTCHA 的標籤，預設在右下方。\u003C\u002Fp>\n","2025-04-02T12:29:05","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F09%2FNuxt_logo.svg.png",{"name":88,"link":89},[104,105],{"name":22,"slug":22},{"name":48,"slug":49},"\u002Fblog\u002F理解與前端實作-recaptcha-v3",{"id":108,"slug":109,"title":110,"description":111,"content":112,"date":113,"image":114,"imageAlt":110,"author":115,"categories":116,"path":119,"minRead":29},3775,"nuxt3-vue-3-動態取得圖片","Nuxt3 \u002F Vue 3 動態取得圖片","Nuxt3 \u002F Vue 3 動態取得圖片 參考資料 https:\u002F\u002Fwww.lichter.io\u002Farticl […]","\u003Ch1>Nuxt3 \u002F Vue 3 動態取得圖片\u003C\u002Fh1>\n\u003Cp>參考資料\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fwww.lichter.io\u002Farticles\u002Fnuxt3-vue3-dynamic-images\u002Fhttps:\u002F\u002Fclairechang.tw\u002F2023\u002F09\u002F12\u002Fnuxt3\u002Fnuxt-v3-assets-vs-public\u002F\">https:\u002F\u002Fwww.lichter.io\u002Farticles\u002Fnuxt3-vue3-dynamic-images\u002Fhttps:\u002F\u002Fclairechang.tw\u002F2023\u002F09\u002F12\u002Fnuxt3\u002Fnuxt-v3-assets-vs-public\u002F\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>把圖片放在assets中後，通常來說會使用這兩種寫法\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;@\u002Fassets\u002Fimage\u002Flogo.jpg&quot;&gt; \n&lt;img src=&quot;..\u002Fassets\u002Fimage\u002Flogo.jpg&quot;&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但只限於靜態圖片，如果想要動態取用圖片。\u003Cbr \u002F>\n例如有10張圖片，檔名是1~10，例如001.jpg ~ 010.jpg\u003C\u002Fp>\n\u003Cp>以前我偷懶會這樣處理：\u003Cbr \u002F>\n放在 public 中，直接取用\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;`\u002Fimage\u002F${index}.jpg`&quot;&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但這種作法有些缺點，\u003C\u002Fp>\n\u003Cul>\n\u003Cli>圖片將被瀏覽器快取住，因此如果我們想要更改圖像，我們也必須更改檔案名稱。這不是最佳的，特別是如果我們想在多個地方使用相同的圖像。\u003C\u002Fli>\n\u003Cli>效能問題，圖片不會被Vite插件之類最佳化。\u003C\u002Fli>\n\u003Cli>圖片將以原始大小加載，如果尺寸很大，同樣也有瀏覽器效能問題。\u003C\u002Fli>\n\u003Cli>管理不一致，這是我的使用習慣，我不喜歡圖片要放在public裡面，比較偏好在assets中一起管理。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>如果換成assets中的話….\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;..\u002Fassets\u002Fimage\u002F${index}.jpg`&quot;&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>會失效。代表取用不到資源\u003C\u002Fp>\n\u003Cp>如果找解決方式可能會找到 Vue2 時代的作法，加入 \u003Cstrong>require，\u003C\u002Fstrong>例如\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img :src=&quot;require(`...\u002Fassets\u002Fimage\u002F${index}.jpg`)&quot; :alt=&quot;selectedDog&quot;&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但這個作法在Vue3 \u002F Nuxt3 不能用了，因為Vite不支援。\u003C\u002Fp>\n\u003Cp>這時要使用另一個Vite的API，\u003Ca href=\"https:\u002F\u002Fvitejs.dev\u002Fguide\u002Ffeatures.html\u002F#glob-import\">\u003Cstrong>\u003Ccode>import.meta.glob\u003C\u002Fcode>\u003C\u002Fstrong>\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>我的理解，\u003Ca href=\"https:\u002F\u002Fvitejs.dev\u002Fguide\u002Ffeatures.html\u002F#glob-import\">\u003Cstrong>\u003Ccode>import.meta.glob\u003C\u002Fcode>\u003C\u002Fstrong>\u003C\u002Fa>有點像是先整理所有引入資料夾中的檔案，建立成一個索引，然後透過key去對應檔名。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;script setup lang=&quot;ts&quot;&gt;\n\nconst glob = import.meta.glob(&#039;@\u002Fassets\u002Fimages\u002F*.jpg&#039;, { eager: true })\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>去檢查 glob 可以看到以下類似資料\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">{\n  &quot;\u002Fassets\u002Fimages\u002Flogo_1`.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_1.jpg&quot;\n  },\n  &quot;\u002Fassets\u002Fimages\u002Flogo_2.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_2.jpg&quot;\n  },\n  &quot;\u002Fassets\u002Fimages\u002Flogo_3.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_3.jpg&quot;\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這時我們用呼叫的圖檔名去尋找真實位置\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img :src=&quot;glob[`\u002Fassets\u002Fimages\u002F${index}.jpg`]&quot; alt=&quot;&quot; \u002F&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>當然也有一些缺點\u003C\u002Fp>\n\u003Cul>\n\u003Cli>圖片名稱會因為Vite快取關係被更改。\u003C\u002Fli>\n\u003Cli>同樣也可能有效能消耗，因為先處理了glob的API。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>就看專案目的取決要使用何種作法囉。\u003C\u002Fp>\n","2024-10-01T00:00:25","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F03%2Ficon-green.svg",{"name":88,"link":89},[117,118],{"name":22,"slug":22},{"name":48,"slug":49},"\u002Fblog\u002Fnuxt3-vue-3-動態取得圖片"]