[{"data":1,"prerenderedAt":267},["ShallowReactive",2],{"navigation":3,"search":26,"$fxsfyuFyRQ":222},[4],{"title":5,"path":6,"stem":7,"children":8,"page":25},"Blog","/blog","blog",[9,13,17,21],{"title":10,"path":11,"stem":12},"Setting Up MSW v2 in Expo Router — What the Docs Don't Tell You","/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)","blog/Integrating MSW v2 with Expo Router (TypeScript + ESModules)",{"title":14,"path":15,"stem":16},"Build a Nuxt Content Blog and Deploy to Cloudflare Pages","/blog/build-nuxt-content-blog","blog/build-nuxt-content-blog",{"title":18,"path":19,"stem":20},"I'm a Junior Frontend Dev Building a Real Product with React Native","/blog/who-i-am","blog/who-I-am",{"title":22,"path":23,"stem":24},"Why Social Media Won't Show Your Blog Title (And How to Fix It in Nuxt)","/blog/why-social-media-wont-show-your-blog-title","blog/why-social-media-wont-show-your-blog-title",false,[27,31,37,43,48,53,58,63,68,73,79,84,89,94,98,103,108,113,118,123,128,133,136,141,146,151,156,161,166,171,174,179,184,189,194,199,204,207,212,217],{"id":11,"title":10,"titles":28,"content":29,"level":30},[],"A step-by-step guide to integrating MSW v2 with Expo Router, covering the undocumented issues and solutions for TypeScript + ESModules setup. I was building a React Native app with Expo Router during a holiday break. The backend API wasn't ready yet, so I needed mock data to build the UI. MSW (Mock Service Worker) is the most popular option for this. Unlike json-server, MSW intercepts requests at the network layer. This means you can write real API calls in your code and keep them unchanged — you just define the same API paths in MSW's handlers. I followed the official MSW docs for React Native integration, but ran into several issues that the docs don't mention. This post walks through the official setup steps, the problems I hit along the way, and how I eventually got it working.",1,{"id":32,"title":33,"titles":34,"content":35,"level":36},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#what-the-official-docs-tell-you-to-do","What the official docs tell you to do",[10],"The MSW team is upfront about this — their React Native docs include this note at the top: This integration is potentially incomplete. If you are a React Native developer, please follow these steps and share any discrepancies/missing pieces with us on GitHub. Let's improve the React Native integration guidelines together. Here are the five steps from the official docs.",2,{"id":38,"title":39,"titles":40,"content":41,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#_1-install-dependencies","1. Install dependencies",[10,33],"npm install msw --save-dev\nnpm install react-native-url-polyfill fast-text-encoding Sources: MSW Quick startReact Native integration",3,{"id":44,"title":45,"titles":46,"content":47,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#_2-create-the-polyfills","2. Create the polyfills",[10,33],"import 'fast-text-encoding'\nimport 'react-native-url-polyfill/auto' Source: React Native integration",{"id":49,"title":50,"titles":51,"content":52,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#_3-define-your-handlers","3. Define your handlers",[10,33],"import { http, HttpResponse } from 'msw'\n\nexport const handlers = [\n    http.get('https://api.example.com/user', () => {\n        return HttpResponse.json({\n            id: 'abc-123',\n            firstName: 'John',\n            lastName: 'Maverick',\n        })\n    }),\n] Sources: MSW Quick startReact Native integration",{"id":54,"title":55,"titles":56,"content":57,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#_4-create-the-server-with-mswnative","4. Create the server with msw/native",[10,33],"import { setupServer } from 'msw/native'\nimport { handlers } from './handlers'\n\nexport const server = setupServer(...handlers) Use msw/native, not msw/node. The /node export patches Node.js's http and https modules, which don't exist in React Native and will crash your app. The /native export only intercepts fetch and XMLHttpRequest — the actual network APIs available in React Native. Sources: MSW Quick startReact Native integration",{"id":59,"title":60,"titles":61,"content":62,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#_5-enable-mocking-in-the-entry-point-of-react-native-application","5. Enable mocking in the entry point of React Native application",[10,33],"import { AppRegistry } from 'react-native'\nimport App from './src/App'\nimport { name as appName } from './app.json'\n\nasync function enableMocking() {\n    if (!__DEV__) {\n        return\n    }\n\n    await import('./msw.polyfills')\n    const { server } = await import('./src/mocks/server')\n    server.listen()\n}\n\nenableMocking().then(() => {\n    AppRegistry.registerComponent(appName, () => App)\n}) Source: React Native integration If you're using Expo Router, Step 5 won't work as written. Here's what happened.",{"id":64,"title":65,"titles":66,"content":67,"level":36},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#what-actually-broke-and-how-i-fixed-it","What actually broke (and how I fixed it)",[10],"",{"id":69,"title":70,"titles":71,"content":72,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#_1-the-entry-point-doesnt-work-with-expo-router","1. The entry point doesn't work with Expo Router",[10,65],"The official docs show you initializing MSW in index.ts before AppRegistry.registerComponent. But with Expo Router, there is no index.ts — the entry point is expo-router/entry, which handles app registration internally. { \"main\": \"expo-router/entry\" } From Expo docs:\nFor the property main, use the expo-router/entry as its value in the package.json. The initial client file is src/app/_layout.tsx (or app/_layout.tsx if not using the src directory). There are two approaches that work. Pick whichever fits your project better.",{"id":74,"title":75,"titles":76,"content":77,"level":78},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#approach-a-custom-entry-point-with-top-level-await","Approach A: Custom entry point with top-level await",[10,65,70],"You can create a custom index.ts that loads MSW first, then imports expo-router/entry: async function enableMocking() {\n    if (!__DEV__) {\n        return;\n    }\n\n    await import(\"./msw.polyfills\");\n    const { server } = await import(\"./mocks/server\");\n    server.listen();\n}\n\nawait enableMocking();\nawait import(\"expo-router/entry\"); { \"main\": \"index.ts\" } Since you're no longer using expo-router/entry directly, you'll also need a type declaration: declare module 'expo-router/entry' A warning about .then(). You might be tempted to write this instead: // ❌ This will crash\nenableMocking().then(() => {\n    import('expo-router/entry')\n}) This crashes with \"Module has not been registered as callable. Registered callable JavaScript modules (n = 0)\". The reason: .then() is non-blocking. The JS engine considers the module \"done loading\" immediately, the native side calls AppRegistry.runApplication(), but the .then() callback hasn't fired yet — so nothing is registered. Top-level await is different. It pauses the entire module's evaluation until the Promise resolves. The module isn't marked as \"loaded\" until all awaits complete, so by the time the native side runs, expo-router/entry has already called registerComponent.",4,{"id":80,"title":81,"titles":82,"content":83,"level":78},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#approach-b-initialize-in-the-root-layout","Approach B: Initialize in the root layout",[10,65,70],"If you prefer not to touch the entry point, you can initialize MSW inside app/_layout.tsx instead. This is Expo Router's first rendered file and the official place for initialization logic. import { Stack } from \"expo-router\";\nimport { useEffect, useState } from \"react\";\nimport { GestureHandlerRootView } from \"react-native-gesture-handler\";\n\nexport default function RootLayout() {\n    const [isMockingReady, setIsMockingReady] = useState(!__DEV__);\n\n    useEffect(() => {\n        async function enableMocking() {\n            if (!__DEV__) return;\n\n            try {\n                await import(\"../msw.polyfills\");\n                const { server } = await import(\"../mocks/server\");\n                server.listen({ onUnhandledRequest: \"bypass\" });\n                console.log(\"[MSW] Mocking enabled\");\n            } catch (error) {\n                console.error(\"[MSW] Setup failed:\", error);\n            }\n\n            setIsMockingReady(true);\n        }\n\n        enableMocking();\n    }, []);\n\n    if (!isMockingReady) {\n        return null;\n    }\n\n    return (\n        \u003CGestureHandlerRootView style={{ flex: 1 }}>\n            \u003CStack>\n                \u003CStack.Screen\n                    name=\"(tabs)\"\n                    options={{ headerShown: false }}\n                />\n            \u003C/Stack>\n        \u003C/GestureHandlerRootView>\n    );\n} useState(!__DEV__) is the key trick here. __DEV__ is a global boolean that Metro injects at build time — true in development, false in production. So !__DEV__ means: in production, isMockingReady starts as true and the app renders immediately with zero delay. In development, it starts as false, shows nothing while MSW initializes, then renders the app once mocking is ready.",{"id":85,"title":86,"titles":87,"content":88,"level":78},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#which-approach-should-you-pick","Which approach should you pick?",[10,65,70],"Approach A (custom entry point) is cleaner — MSW starts before any component renders, so there's no loading state to manage. But it relies on top-level await support in Metro, which may not work in older Expo SDK versions. Approach B (root layout) is more conservative. It doesn't depend on any special module loading behavior, just standard React lifecycle. The trade-off is a brief blank screen during MSW initialization in development. I use Approach A in my project, but either works.",{"id":90,"title":91,"titles":92,"content":93,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#_2-the-polyfills-are-incomplete","2. The polyfills are incomplete",[10,65],"After fixing the entry point, I got this error: Uncaught Error: Property 'MessageEvent' doesn't exist Followed by: TypeError: Cannot read property 'listen' of undefined The second error is a consequence of the first. When MessageEvent is missing, setupServer() silently fails and returns undefined. Then calling server.listen() crashes because server is undefined. The official polyfills file only includes URL and TextEncoder. But MSW v2 internally references four additional browser APIs — MessageEvent, Event, EventTarget, and BroadcastChannel — none of which exist in Hermes (React Native's JS engine). MSW uses these for WebSocket support, but it references them even when you're only mocking HTTP. This has been reported as an open issue on the MSW docs repo. The fix comes from a community blog post — add minimal stubs for the missing globals: import 'fast-text-encoding'\nimport 'react-native-url-polyfill/auto'\n\nfunction defineMockGlobal(name: string) {\n    if (typeof (global as any)[name] === 'undefined') {\n        ;(global as any)[name] = class {\n            type: string\n            constructor(type: string, eventInitDict?: Record\u003Cstring, any>) {\n                this.type = type\n                Object.assign(this, eventInitDict)\n            }\n        }\n    }\n}\n\n;['MessageEvent', 'Event', 'EventTarget', 'BroadcastChannel'].forEach(\n    defineMockGlobal,\n) These aren't full implementations — they're minimal stubs with just enough structure to prevent MSW from crashing during initialization. MSW references these classes internally for WebSocket support, even when you're only mocking HTTP.",{"id":95,"title":96,"titles":97,"content":67,"level":36},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#things-i-learned-along-the-way","Things I learned along the way",[10],{"id":99,"title":100,"titles":101,"content":102,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#how-react-native-startup-actually-works","How React Native startup actually works",[10,96],"React Native apps have two separate sides running at the same time: the native side (Objective-C on iOS, Kotlin on Android) that controls the actual screen, and the JS side (your React code running in Hermes). They communicate through a bridge — the native side sends events to JS, and JS sends UI updates back. The important part: these two sides don't share a call stack.\nThe native side can't await anything on the JS side. It fires a message and moves on. So here's what happens at startup: The native side tells Metro to load the JS bundleThe JS bundle executes top-to-bottom — this is where registerComponent needs to runOnce the bundle is marked as \"loaded\", the native side calls AppRegistry.runApplication() If registerComponent hasn't been called by step 3, the native side finds nothing registered and crashes with \"Registered callable JavaScript modules (n = 0)\". The key insight is: the native side waits for the bundle to finish loading, but not for JS Promises to resolve. This is the difference between .then() and top-level await.",{"id":104,"title":105,"titles":106,"content":107,"level":78},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#why-then-crashes","Why .then() crashes:",[10,96,100],"enableMocking().then(() => { import('expo-router/entry') }) .then() schedules a callback but doesn't block execution. As far as the JS engine is concerned, this module is done — it executed every line. So the bundle is marked as \"loaded\" and the native side fires runApplication(). But the .then() callback hasn't run yet, so registerComponent was never called.",{"id":109,"title":110,"titles":111,"content":112,"level":78},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#why-top-level-await-works","Why top-level await works:",[10,96,100],"await enableMocking()\nawait import('expo-router/entry') Top-level await tells the JS engine: \"this module is not finished evaluating yet.\" The bundle stays in a \"still loading\" state until all awaits resolve. The native side doesn't fire runApplication() until the bundle is fully loaded — and by that point, expo-router/entry has already run and registered the component. Source: React Native AppRegistry docs",{"id":114,"title":115,"titles":116,"content":117,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#static-imports-get-hoisted-you-cant-control-their-order","Static imports get hoisted — you can't control their order",[10,96],"When I first tried to fix the entry point, I assumed I could just reorder my imports: // I expected this to run top-to-bottom\nimport './msw.polyfills'\nimport './mocks/server'\nimport 'expo-router/entry' This doesn't work because static import statements are hoisted — the JS engine moves them all to the top and runs them before any other code, regardless of where you write them. You can't guarantee order between static imports. Dynamic import() is different. It returns a Promise and runs exactly when you call it: // This actually runs in order\nawait import('./msw.polyfills')\nawait import('./mocks/server')\nawait import('expo-router/entry') Each await waits for the previous import to complete before starting the next one. No hoisting, no reordering.",{"id":119,"title":120,"titles":121,"content":122,"level":42},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#what-__dev__-actually-is","What __DEV__ actually is",[10,96],"It's not magic — it's literally a variable that Metro (the bundler) writes into the first line of your JS bundle at build time. In a development bundle, Metro writes var __DEV__ = true. In a production bundle, it writes var __DEV__ = false. This means if (!__DEV__) return isn't a runtime check against some environment variable. It's a check against a hardcoded boolean. And because it's hardcoded, production builds can potentially tree-shake the entire MSW code path away.",{"id":124,"title":125,"titles":126,"content":127,"level":36},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#final-file-structure","Final file structure",[10],"├── app/\n│   └── _layout.tsx\n├── mocks/\n│   ├── handlers.ts\n│   └── server.ts          # uses msw/native\n├── msw.polyfills.ts        # 2 packages + 4 global stubs\n├── index.ts                # only if using Approach A\n├── types/\n│   └── expo-router-entry.d.ts  # only if using Approach A\n└── package.json",{"id":129,"title":130,"titles":131,"content":132,"level":36},"/blog/integrating-msw-v2-with-expo-router-(typescript-+-esmodules)#sources","Sources",[10],"MSW React Native integrationExpo Router installation — entry pointExpo Router — core conceptsExpo Router — layoutReact Native AppRegistryCommunity polyfill fix (velog.io/@gs0428)MSW docs repo — open polyfill issue #453 html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"id":15,"title":14,"titles":134,"content":135,"level":30},[],"A practical walkthrough of building a content-driven blog with Nuxt Content and deploying it to Cloudflare Pages.",{"id":137,"title":138,"titles":139,"content":140,"level":36},"/blog/build-nuxt-content-blog#tldr","TL;DR",[14],"轉職 Vue 前端後近半年意識到也該寫一個部落格，畢竟嘗試著將問題與決策闡述對於加深理解和思考很有幫助。一開始是在看到了 Josh Comeau 的網站才有的想法，他在文章提供了許多互動式的組件很容易方便讀者理解，我也認為在寫這種互動式組件範例的時候也會同時加深自己對於概念和實作的理解。 為了嘗試 Nuxt 和 SSR 所以選擇 Nuxt Content，Cloudflare pages 不像其他的平台需要付費，而且 Nuxt Content 官方文檔也有提供部署 Cloudflare pages 的說明文件，就很快簡單的決定了路線。",{"id":142,"title":143,"titles":144,"content":145,"level":36},"/blog/build-nuxt-content-blog#nuxt-installation-nuxt-content-doc","Nuxt Installation (Nuxt Content Doc)",[14],"先建立專案（create-nuxt CLI）npm create nuxt \u003Cproject-name> 執行後會有幾個選項需要選擇： Which template would you like to use? → content – Content-driven website 因為是要用 Nuxt ContentWhich package manager would you like to use? → npm (current) 選自己習慣的就好Initialize git repository? → Yes 新專案所以順便 git 初始化，等等丟上 Github 讓 Cloudflare 可以連接Would you like to install any of the official modules? → Yes@nuxt/ui （幾乎是 Nuxt 標配，加上我也沒碰過，所以想練習一下）@nuxt/icon（看會不會用 icon）其他都可以之後再安裝，但我還選了 @nuxt/eslint ＆ @nuxt/imageDo you want to install better-sqlite3 package? → Yes",{"id":147,"title":148,"titles":149,"content":150,"level":36},"/blog/build-nuxt-content-blog#nuxt-content-config","Nuxt content config",[14],"這邊目前都暫時按照 Nuxt Content Doc 的步驟來。 成功建立專案後就可以在跟目錄底下建立 content.config.ts 這是 Nuxt Content 主要的設定檔，用來定義如何組織、解析與管理內容的設定檔。\nimport { defineContentConfig, defineCollection } from '@nuxt/content'\n\nexport default defineContentConfig({\n  collections: {\n    content: defineCollection({\n      // Specify the type of content in this collection\n      type: 'page',\n      // Load every file inside the `content` directory\n      source: '**/*.md'\n    })\n  }\n})\ncollections 底下的每個 collection 會對應到特定的內容目錄與結構，讓系統能夠統一索引、查詢與渲染這些內容，例如部落格文章或文件。建立專案的第一個 Markdown Page\n# My First Page\n\nHere is some content.\n建立 pages/index.vue 這會顯示你頁面的內容（也就是 /content 目錄裡對應的目錄與結構 ）參考 Nuxt Content ContentRenderer ComponentsThe \u003CContentRenderer> component renders a document coming from a query with queryCollection().\u003Cscript setup lang=\"ts\">\nconst { data: home } = await useAsyncData(() =>\n  queryCollection('content').path('/').first()\n)\n\nuseSeoMeta({\n  title: home.value?.title,\n  description: home.value?.description\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003CContentRenderer v-if=\"home\" :value=\"home\" />\n  \u003Cdiv v-else>Home not found\u003C/div>\n\u003C/template>\n因為上面在 content.config.ts 的 collections 已經定義好 content 這個 collection，所以可以用 queryCollection('content') 取得。",{"id":152,"title":153,"titles":154,"content":155,"level":36},"/blog/build-nuxt-content-blog#deploy-to-cloudflare-pages","Deploy to Cloudflare pages",[14],"Nuxt Content - Deploy Cloudflare Pages 寫了 Nuxt content 必需要連接 D1 database 才能正常運行。",{"id":157,"title":158,"titles":159,"content":160,"level":42},"/blog/build-nuxt-content-blog#step1-create-d1-database","Step1. Create D1 Database",[14,153],"打開 Cloudflare 在側邊欄找到 Storage & databases / D1 SQL Database → 新增 Database，名字取自己喜歡的就好",{"id":162,"title":163,"titles":164,"content":165,"level":42},"/blog/build-nuxt-content-blog#step2-add-new-config-in-nuxtconfigts-for-deployment","Step2. Add new config in nuxt.config.ts for deployment",[14,153],"在專案內添加設定 // https://nuxt.com/docs/api/configuration/nuxt-config\nexport default defineNuxtConfig({\n  ...,\n  content: {\n    database: {\n      type: 'd1',\n      // It corresponed to Cloudflare pages variable, show it later.\n      bindingName: 'DB',\n    }\n  },\n  nitro: {\n    preset: 'cloudflare_pages',\n    prerender: {\n      // Prevent trailing slash problem\n      autoSubfolderIndex: false,\n    }\n  }\n})",{"id":167,"title":168,"titles":169,"content":170,"level":42},"/blog/build-nuxt-content-blog#step3-create-new-cloudflare-pages-application","Step3. Create new Cloudflare pages application",[14,153],"打開 Cloudflare 在最上方找到 +Add 按鈕選擇 Pages選擇第一個選項，然後直接選擇對應的專案部署設定 Framework preset 選擇 Nuxt 然後新增變數，變數名稱就是我們剛剛在 nuxt.config.ts 寫的 bindingName: DB ，對應的值就是我們前面新增 D1 database 時取的名字部署成功後就會變成以下的畫面，如果有已經購買網域的可以點選 Add custom domain 去設定成自己購買的網域 html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":19,"title":18,"titles":172,"content":173,"level":30},[],"A junior frontend developer from Taiwan sharing the journey of building a real booking app with React Native.",{"id":175,"title":176,"titles":177,"content":178,"level":36},"/blog/who-i-am#who-i-am","Who I Am",[18],"I'm a frontend developer from Taiwan, currently working with Vue 3 and TypeScript at my day job. I've been a frontend developer for a few months. Most of my experience is in web development — I'd call myself a junior who's still figuring things out every day. I also built this blog with Nuxt Content, which has been another learning adventure on its own.",{"id":180,"title":181,"titles":182,"content":183,"level":36},"/blog/who-i-am#what-im-building-and-why","What I'm Building (and Why)",[18],"I noticed that industries heavily dependent on human interaction still rely on phone calls and LINE messages for booking appointments. It's messy for both the business owners and their customers. I'm also a lazy person who doesn't like unnecessary friction that makes me reluctant to deal with these things. So I decided to build a booking app designed specifically for this kind of business.",{"id":185,"title":186,"titles":187,"content":188,"level":36},"/blog/who-i-am#why-react-native-coming-from-vue","Why React Native (Coming from Vue)",[18],"My day-to-day stack is Vue 3 + TypeScript, so picking up React Native was a big leap. I chose RN because I wanted to build a mobile app without learning Swift and Kotlin separately, and I figured the React ecosystem would be worth investing in long-term. The mental model shift from Vue's reactivity to React's useState was harder than I expected.",{"id":190,"title":191,"titles":192,"content":193,"level":36},"/blog/who-i-am#why-build-in-public","Why Build in Public",[18],"I'm doing this half as practice, half as a real product. Being public about it forces me to keep going even when progress is slow. As a junior, I run into problems that senior devs have forgotten about. Documenting these struggles might help someone going through the same thing. I don't want to pretend I have it all figured out. I'm learning as I go, and I think there's value in showing that process honestly.",{"id":195,"title":196,"titles":197,"content":198,"level":36},"/blog/who-i-am#where-i-am-right-now","Where I Am Right Now",[18],"Here's where things stand: Tech stack: React Native with Expo RouterCurrent progress: I've just scaffolded the project and am about to build the first feature.Biggest challenge so far: The architecture and React concepts that always make me research for a long time before I can start.Next step: Building the time slot selection logic and talking to a few local shop owners",{"id":200,"title":201,"titles":202,"content":203,"level":36},"/blog/who-i-am#what-to-expect-from-this-series","What to Expect from This Series",[18],"I'll be sharing my progress regularly — the technical wins, the dumb mistakes, and everything in between. Some posts will be dev tutorials (like how I set up MSW), and some will be about the product side (like what I learned from talking to potential users). But this blog isn't just about the booking app. I'm also still strengthening my JavaScript fundamentals and learning Nuxt as I build this site, so expect posts about those topics too — like revisiting async/await, or how I added a new feature to this blog. It's all part of the same learning journey. If you're also a junior dev trying to build something real, I hope this series makes you feel less alone in the process.",{"id":23,"title":22,"titles":205,"content":206,"level":30},[],"How I fixed missing link previews on Threads by adding Open Graph tags to my Nuxt Content blog. I just published a post — Integrating MSW v2 with Expo Router (TypeScript + ESModules) — on my Nuxt Content blog and wanted to share it on Threads. But when I pasted the link, the preview didn't show the article title.",{"id":208,"title":209,"titles":210,"content":211,"level":36},"/blog/why-social-media-wont-show-your-blog-title#whats-happening-and-what-do-i-do","What's Happening and What Do I Do",[22],"The platform is fetching my page but displaying the wrong information. So I searched \"How can I enable link previews for my blog on social media?\" and found this Stack Overflow answer — How do social media sites get a preview of a linked page? — which was pretty straightforward: I see you've tagged this post with Facebook. If you are concerned with Link Previews as they appear when shared to Facebook, here is some relevant information for you. There is something called Open Graph tags that web developers can put on their pages. These are scraped/retrieved by various services that provide previews. For example, see Facebook's Sharing Debugger Tool along with an explanation of how it works. You can paste your URL directly into Facebook's Sharing Debugger Tool to test the preview, or use opengraph.xyz or metatags.io to see which OG tags your page currently has and how it looks across different platforms. Open Graph tags are scraped by services that generate link previews.\nMeta Developers – Sharing for Webmasters explains:\nWithout these Open Graph tags, the Facebook Crawler uses internal heuristics to make a best guess about the title, description, and preview image for your content.",{"id":213,"title":214,"titles":215,"content":216,"level":36},"/blog/why-social-media-wont-show-your-blog-title#how-to-add-og-tags-in-nuxt","How to Add OG Tags in Nuxt",[22],"Searching that title brings up SEO and Meta · Get Started with Nuxt v4 as one of the first results. The docs assume you already know what HTML \u003Chead> and \u003Cbody> are. In short: \u003Chead>\n  \u003C!-- Invisible to users, read by browsers and crawlers -->\n  \u003Cmeta property=\"og:title\" content=\"Your Article Title\" />\n\u003C/head>\n\u003Cbody>\n  \u003C!-- What users actually see -->\n\u003C/body> Scrolling down to useSeoMeta, the description alone — \"SEO meta tags\" — might not immediately tell you this is about OG tags. But once you see ogTitle and ogImage in the code example, it clicks. Looking at the useSeoMeta API docs alongside Meta Developers – Sharing for Webmasters: Basic Tags and The Open Graph protocol, these are the essential tags: og:title - The title of your pageog:description - A brief descriptionog:type - Content type, use article for blog postsog:image - Full URL of the preview imageog:url - The full URL of this page ogUrl requires a full absolute URL (e.g. https://richiea1y.com/blog/...), but route.path inside a Vue file only gives you a relative path (/blog/...). So you need to prepend your domain manually: ogUrl: `https://richiea1y.com${route.path}` Note: don't add a trailing / to your domain, since route.path already starts with / — otherwise you'll get https://richiea1y.com//blog/.... Here's what [...slug].vue looks like after adding useSeoMeta: \u003Cscript setup lang=\"ts\">\nconst route = useRoute()\n\nconst { data: post } = await useAsyncData('blog-' + route.path, () =>\n  queryCollection('blog').path(route.path).first()\n)\n\nif (!post.value) {\n  throw createError({\n    statusCode: 404,\n    statusMessage: 'Post not found',\n    fatal: true,\n  })\n}\n\nuseSeoMeta({\n  title: post.value.title,\n  ogTitle: post.value.title,\n  description: post.value.description,\n  ogDescription: post.value.description,\n  ogType: 'article',\n  ogUrl: `https://richiea1y.com${route.path}`,\n})\n\u003C/script> Also make sure to add a description field to your post's frontmatter — otherwise post.value.description will be undefined: ---\ntitle: Your Article Title\ndescription: A sentence or two about what this post covers.\ndate: 2026-02-28\n--- Source:\nNuxt useSeoMeta()Open Graph ProtocolMeta Developers – Sharing for WebmastersTwitter Cards",{"id":218,"title":219,"titles":220,"content":221,"level":36},"/blog/why-social-media-wont-show-your-blog-title#validation","Validation",[22],"After deploying, I confirmed the title was showing correctly on opengraph.xyz. Here's what the Threads preview looks like now: You can use these tools to verify your OG tags are set up correctly: Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/Twitter Card Validator: https://cards-dev.twitter.com/validatorOpen Graph Checker: https://www.opengraph.xyz/ html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"id":223,"title":224,"body":225,"description":259,"extension":260,"meta":261,"navigation":262,"path":263,"seo":264,"stem":265,"__hash__":266},"content/about.md","About",{"type":226,"value":227,"toc":257},"minimark",[228,237,240,243,246],[229,230,231,232,236],"p",{},"Hey, ",[233,234,235],"strong",{},"I'm Richard"," — a junior frontend developer from Taiwan.",[229,238,239],{},"I work with Vue 3 and TypeScript during the day, and I'm currently building a booking app with React Native on the side.",[229,241,242],{},"This blog is where I document what I'm learning: JavaScript fundamentals, Vue and Nuxt, React Native, and whatever else I'm figuring out along the way. Some posts are tutorials, some are notes-to-self, and some are just me thinking out loud.",[229,244,245],{},"I built this site with Nuxt Content, and I'm still tweaking it as I learn.",[229,247,248,251,252],{},[233,249,250],{},"If anything here helps you, or if you're on a similar journey, feel free to reach out",". ",[253,254,256],"a",{"href":255},"/","Check out my blog",{"title":67,"searchDepth":36,"depth":36,"links":258},[],"Hey, I'm Richard — a junior frontend developer from Taiwan.","md",{},true,"/about",{"description":259},"about","ewPz5B48-CYbvyUKcYs2xm5zpd85_i1gPv4IgEG_RSI",1778054783758]