[{"data":1,"prerenderedAt":270},["ShallowReactive",2],{"post-environment-variables-for-static-sites":3,"blog-posts":14},{"_path":4,"title":5,"description":6,"date":7,"tags":8,"readingTime":12,"body":13},"\u002Fblog\u002Fenvironment-variables-for-static-sites\u002F","Environment Variables for Static Sites","How to use build-time env vars in Nuxt and other static generators without leaking secrets or breaking CI.","2025-10-05",[9,10,11],"nuxt","devops","frontend",5,"# Environment Variables for Static Sites\n\nStatic sites do not have a server to read secrets at request time. Every `process.env` value you rely on is baked in at build time—which is both a feature and a footgun if you treat client env like a private vault.\n\n## Public vs private\n\nAnything prefixed for the client bundle is visible in the shipped JavaScript. Treat those values as public: analytics IDs, public API base URLs, feature flags you are fine exposing.\n\nNever put API keys, tokens, or private credentials in client-side env vars. If a value must stay secret, keep it on a server or edge function—not in a static export. “Obfuscated” is not “secret.” Anyone can open DevTools, search the bundle, or read the network tab.\n\nA practical split I use:\n\n| Kind of value | Where it belongs |\n| --- | --- |\n| Site URL, public CDN base | Client \u002F public runtime config |\n| Analytics measurement ID | Client \u002F public (expected to be visible) |\n| Private API token | Server-only env or edge secret |\n| Webhook signing secret | Server-only; never `NUXT_PUBLIC_*` |\n\nWhen a third-party dashboard shows you an “API key,” check whether it is meant for browsers. Many services give you a publishable key (safe-ish in the client) and a secret key (server only). Mixing those up is one of the most common static-site leaks I see in reviews.\n\nIf you are tempted to hide a key “just for this weekend prototype,” assume the prototype will ship. Put the secret behind a function from day one, or use a throwaway key you can revoke without drama.\n\n## Nuxt runtime config\n\nNuxt separates public and private config so you do not have to invent your own convention:\n\n```typescript\nexport default defineNuxtConfig({\n  runtimeConfig: {\n    apiSecret: process.env.API_SECRET, \u002F\u002F server-only\n    public: {\n      siteUrl: process.env.NUXT_PUBLIC_SITE_URL\n    }\n  }\n})\n```\n\nIn components and composables, read public values through `useRuntimeConfig().public`. Private keys stay on the server side of Nitro—pages, server routes, or middleware that never ship to the browser.\n\nFor a Cloudflare Pages deploy with Nitro, keep secrets in server-only runtime config (or Pages project env vars) and put only `public` values in the client bundle. Pages will inject env at build\u002Fruntime according to how you configure the project; your job is to make sure the names match what `nuxt.config` expects.\n\nA few habits that prevent surprises:\n\n- Prefer `NUXT_PUBLIC_*` for values that must be available at build for prerendered HTML\n- Avoid reading `process.env` directly inside client components; go through runtime config\n- Fail loudly in CI if a required public var is missing—empty strings become mysterious broken links later\n\nExample of a small server route that uses a private secret without exposing it:\n\n```typescript\n\u002F\u002F server\u002Fapi\u002Fcontact.post.ts\nexport default defineEventHandler(async (event) => {\n  const config = useRuntimeConfig()\n  const body = await readBody(event)\n\n  \u002F\u002F config.apiSecret never appears in the client bundle\n  await $fetch('https:\u002F\u002Fapi.example.com\u002Fmessages', {\n    method: 'POST',\n    headers: { Authorization: `Bearer ${config.apiSecret}` },\n    body\n  })\n\n  return { ok: true }\n})\n```\n\nStatic output can still include API routes when your host supports them. The static pages stay public; the secret stays on the edge.\n\n## Keep CI and local in sync\n\nDocument required variables in the README. Use a `.env.example` with placeholder values so contributors know what to set—without committing real secrets:\n\n```bash\n# .env.example\nNUXT_PUBLIC_SITE_URL=https:\u002F\u002Fexample.com\nAPI_SECRET=replace-me\n```\n\nAdd `.env` to `.gitignore`. When a build fails only in CI, the first check is whether the workflow or host dashboard defines the same env vars your local `.env` has. The second check is whether the *names* match exactly—including the `NUXT_PUBLIC_` prefix.\n\nI keep a short checklist next to deploy docs:\n\n1. Local `.env` works for `npm run dev` and `npm run build`\n2. CI \u002F Pages project has the same keys for production\n3. Preview environments get safe defaults (often a staging site URL, never production secrets you cannot rotate)\n4. Someone new can copy `.env.example` and know which values are required vs optional\n\nAlso watch for “works in preview, broken in production” caused by env set only on one environment. Cloudflare Pages and similar hosts let you scope variables per environment—use that deliberately instead of assuming one global set.\n\nIf a value changes how prerendered HTML looks (canonical URLs, feature flags baked into markup), rebuild after changing it. Static sites do not pick up new env at request time for HTML that was already generated.\n\nOne more footgun: committing a real `.env` “just this once” to unblock a teammate. Rotate anything that touched git history, even briefly, and prefer a password manager or host dashboard invite instead. Secrets in commits are forever unless you rewrite history—and rewriting shared history is worse than the original shortcut.\n\n## Wrap-up\n\nStatic hosting rewards boring env hygiene: public values only in the bundle, secrets off the client entirely, and the same variable names in local dev and CI. Get that right once and deploys stop being guesswork—and you avoid the worst kind of incident report: a secret that was never supposed to be public in the first place.",[15,23,33,42,49,57,64,72,79,86,93,101,108,115,122,130,136,144,151,159,165,172,178,186,194,201,203,209,216,222,228,234,241,248,255,263],{"_path":16,"title":17,"description":18,"date":19,"tags":20,"readingTime":12},"\u002Fblog\u002Fclient-side-tools-that-earn-trust\u002F","Client-Side Tools That Earn Trust","How to design browser tools that feel private by default—local processing, clear data boundaries, and UX that never asks people to guess where their files go.","2026-07-20",[21,11,22],"privacy","tools",{"_path":24,"title":25,"description":26,"date":27,"tags":28,"readingTime":32},"\u002Fblog\u002Fusb-c-cables-that-actually-matter\u002F","USB-C Cables That Actually Matter","How to buy USB-C cables by wattage and data speed so you stop guessing which cord charges slowly or fails a file transfer.","2026-07-14",[29,30,31],"gadgets","hardware","productivity",4,{"_path":34,"title":35,"description":36,"date":37,"tags":38,"readingTime":12},"\u002Fblog\u002Fcolor-contrast-people-can-read\u002F","Color Contrast People Can Actually Read","Quick checks for text and UI colors that stay legible in light mode, dark mode, and on mediocre displays.","2026-07-07",[39,40,41],"accessibility","css","design",{"_path":43,"title":44,"description":45,"date":46,"tags":47,"readingTime":12},"\u002Fblog\u002Fportable-ssds-worth-carrying\u002F","Portable SSDs Worth Carrying","When a pocket SSD beats cloud sync for travel, backups, and large media—and how to pick one that stays fast and durable.","2026-06-26",[29,48,31],"storage",{"_path":50,"title":51,"description":52,"date":53,"tags":54,"readingTime":12},"\u002Fblog\u002Fwireless-earbuds-that-last-the-day\u002F","Wireless Earbuds That Last the Day","Simple charging and fit habits that stretch earbud battery life without babysitting the case every few hours.","2026-06-18",[29,55,56],"audio","habits",{"_path":58,"title":59,"description":60,"date":61,"tags":62,"readingTime":32},"\u002Fblog\u002Fwhen-a-second-monitor-helps\u002F","When a Second Monitor Actually Helps","A practical take on dual screens for coding, writing, and research—plus when a single good display is still the better desk.","2026-06-10",[29,31,63],"workspace",{"_path":65,"title":66,"description":67,"date":68,"tags":69,"readingTime":32},"\u002Fblog\u002Fshort-session-game-picks\u002F","Short-Session Games That Respect Your Evening","Genres and design patterns that fit a 30-minute window without the “one more hour” trap.","2026-06-04",[70,56,71],"gaming","indie",{"_path":73,"title":74,"description":75,"date":76,"tags":77,"readingTime":12},"\u002Fblog\u002Fphone-charging-habits-that-age-better\u002F","Phone Charging Habits That Age Better","Practical charging routines that keep a phone useful longer—without obsessing over the perfect battery percentage.","2026-05-23",[29,78,56],"mobile",{"_path":80,"title":81,"description":82,"date":83,"tags":84,"readingTime":12},"\u002Fblog\u002Ffinishing-side-projects-without-burning-out\u002F","Finishing Side Projects Without Burning Out","A practical playbook for shrinking scope, shipping thin vertical slices, and keeping personal projects fun long enough to actually launch.","2026-05-14",[31,85,56],"side-projects",{"_path":87,"title":88,"description":89,"date":90,"tags":91,"readingTime":12},"\u002Fblog\u002Faccessible-forms-people-finish\u002F","Accessible Forms That People Actually Finish","Practical accessibility checks for labels, errors, and focus so your forms work for more users.","2026-05-07",[39,92,11],"html",{"_path":94,"title":95,"description":96,"date":97,"tags":98,"readingTime":12},"\u002Fblog\u002Fvue-composition-api-patterns\u002F","Practical Vue Composition API Patterns","Reusable patterns for composables, shared state, and cleaner component logic in Vue 3.","2026-04-27",[99,100,11],"vue","javascript",{"_path":102,"title":103,"description":104,"date":105,"tags":106,"readingTime":12},"\u002Fblog\u002Fdesigning-empty-states-that-teach\u002F","Designing Empty States That Teach the Product","Empty screens are not placeholders—they are onboarding. How to write, layout, and sequence first-run UI so people know what to do next.","2026-04-19",[107,11,41],"ux",{"_path":109,"title":110,"description":111,"date":112,"tags":113,"readingTime":12},"\u002Fblog\u002Fwhy-indie-games-keep-winning\u002F","Why Indie Games Keep Winning Attention","How small teams punch above their weight with focused design, personality, and smart scope.","2026-04-13",[70,71,114],"industry",{"_path":116,"title":117,"description":118,"date":119,"tags":120,"readingTime":12},"\u002Fblog\u002Fkeeping-component-props-simple\u002F","Keeping Component Props Simple","When to split a component, when to use slots, and how to avoid prop objects that grow forever.","2026-04-03",[99,11,121],"best-practices",{"_path":123,"title":124,"description":125,"date":126,"tags":127,"readingTime":12},"\u002Fblog\u002Fcalm-frontend-performance\u002F","A Calm Approach to Frontend Performance","Performance work that sticks: measure what users feel, fix the critical path first, and avoid optimization theater on personal and portfolio sites.","2026-03-26",[128,11,129],"performance","web",{"_path":131,"title":132,"description":133,"date":134,"tags":135,"readingTime":12},"\u002Fblog\u002Fcss-layout-without-the-hacks\u002F","CSS Layout Without the Hacks","Modern Flexbox and Grid techniques that replace brittle floats, magic numbers, and overflow tricks.","2026-03-17",[40,11,41],{"_path":137,"title":138,"description":139,"date":140,"tags":141,"readingTime":12},"\u002Fblog\u002Fcloud-gaming-what-gets-in-the-way\u002F","Cloud Gaming: What Still Gets in the Way","Latency, input feel, and library lock-in—why streaming games is impressive and still uneven.","2026-03-10",[70,142,143],"cloud-gaming","tech",{"_path":145,"title":146,"description":147,"date":148,"tags":149,"readingTime":12},"\u002Fblog\u002Fshipping-static-sites-with-nuxt\u002F","Shipping Static Sites with Nuxt","How I build and deploy a Nuxt site to Cloudflare Pages with predictable routes and content.","2026-03-02",[9,150,10],"ssg",{"_path":152,"title":153,"description":154,"date":155,"tags":156,"readingTime":12},"\u002Fblog\u002Fdebugging-faster-in-the-browser\u002F","Debugging Faster in the Browser","A short toolkit of DevTools habits—breakpoints, network filters, and DOM inspection—that cut debug time.","2026-02-24",[100,157,158],"devtools","debugging",{"_path":160,"title":161,"description":162,"date":163,"tags":164,"readingTime":12},"\u002Fblog\u002Fsmall-javascript-habits-that-scale\u002F","Small JavaScript Habits That Scale","Everyday habits—naming, early returns, and clearer async flow—that keep front-end codebases maintainable.","2026-02-18",[100,121,11],{"_path":166,"title":167,"description":168,"date":169,"tags":170,"readingTime":32},"\u002Fblog\u002Fhealthier-gaming-routine\u002F","Building a Healthier Gaming Routine","Simple habits for sessions that stay fun—breaks, goals, and knowing when to stop.","2026-02-02",[70,56,171],"wellness",{"_path":173,"title":174,"description":175,"date":176,"tags":177,"readingTime":12},"\u002Fblog\u002Fresponsive-images-without-layout-shift\u002F","Responsive Images Without Layout Shift","Width, height, srcset, and lazy loading patterns that keep pages stable while images load.","2026-01-22",[92,128,11],{"_path":179,"title":180,"description":181,"date":182,"tags":183,"readingTime":12},"\u002Fblog\u002Fgit-habits-for-small-teams\u002F","Git Habits for Solo and Small Teams","Branch naming, commit messages, and PR hygiene that keep history useful without slowing you down.","2026-01-09",[184,185,31],"git","workflow",{"_path":187,"title":188,"description":189,"date":190,"tags":191,"readingTime":12},"\u002Fblog\u002Fwhat-makes-a-game-stream-worth-watching\u002F","What Makes a Game Stream Worth Watching","Commentary, pacing, and community—why some streams click even when the gameplay is messy.","2025-12-14",[70,192,193],"streaming","content",{"_path":195,"title":196,"description":197,"date":198,"tags":199,"readingTime":12},"\u002Fblog\u002Fwhen-to-reach-for-typescript-vue\u002F","When to Reach for TypeScript on a Vue App","A pragmatic take on where TypeScript pays off in Vue—and where plain JS is still fine.","2025-11-30",[200,99,11],"typescript",{"_path":4,"title":5,"description":6,"date":7,"tags":202,"readingTime":12},[9,10,11],{"_path":204,"title":205,"description":206,"date":207,"tags":208,"readingTime":12},"\u002Fblog\u002Fpicking-a-side-project-stack\u002F","Picking a Side Project Stack Without Overthinking It","How to choose tools for a personal project when the goal is shipping, not building the perfect architecture.","2025-09-18",[31,11,121],{"_path":210,"title":211,"description":212,"date":213,"tags":214,"readingTime":32},"\u002Fblog\u002Fco-op-games-for-busy-friends\u002F","Co-op Games That Work When Everyone Is Busy","Async-friendly and drop-in co-op picks for friend groups that rarely share the same free evening.","2025-08-22",[70,215,56],"co-op",{"_path":217,"title":218,"description":219,"date":220,"tags":221,"readingTime":12},"\u002Fblog\u002Fsemantic-html-still-matters\u002F","Semantic HTML Still Matters","Landmarks, headings, and native elements that make pages easier to use, style, and maintain.","2025-07-06",[92,39,11],{"_path":223,"title":224,"description":225,"date":226,"tags":227,"readingTime":12},"\u002Fblog\u002Fgit-without-the-jargon\u002F","Learning Git Without the Jargon","A plain-language mental model for commits, branches, and merges when tutorials assume you already know Git.","2025-05-14",[184,185,31],{"_path":229,"title":230,"description":231,"date":232,"tags":233,"readingTime":12},"\u002Fblog\u002Fwhy-loading-states-matter\u002F","Why Loading States Matter More Than Animations","Skeletons, disabled buttons, and honest feedback beat flashy spinners when data takes time to arrive.","2025-03-28",[107,11,41],{"_path":235,"title":236,"description":237,"date":238,"tags":239,"readingTime":12},"\u002Fblog\u002Fnaming-things-so-future-you-survives\u002F","Naming Things So Future You Survives","Practical rules for naming variables, components, and files so a codebase stays readable months after the clever jokes fade.","2025-03-10",[100,121,240],"maintainability",{"_path":242,"title":243,"description":244,"date":245,"tags":246,"readingTime":12},"\u002Fblog\u002Fsave-systems-that-respect-time\u002F","Save Systems That Respect the Player’s Time","Checkpoints, autosaves, and manual slots are design choices. How thoughtful saving reduces rage-quits without deleting tension from the game.","2025-02-28",[70,247,107],"game-design",{"_path":249,"title":250,"description":251,"date":252,"tags":253,"readingTime":12},"\u002Fblog\u002Fmicrocopy-that-makes-interfaces-human\u002F","Microcopy That Makes Interfaces Feel Human","Button labels, error lines, and helper text are product design. How to write UI words that reduce hesitation without sounding like a chatbot.","2025-01-22",[107,254,41],"writing",{"_path":256,"title":257,"description":258,"date":259,"tags":260,"readingTime":12},"\u002Fblog\u002Freadmes-that-get-projects-running\u002F","READMEs That Get Projects Running","How to write a README that helps someone—including future you—install, run, and understand a project without opening a support chat.","2024-12-18",[261,262,56],"documentation","open-source",{"_path":264,"title":265,"description":266,"date":267,"tags":268,"readingTime":12},"\u002Fblog\u002Fprogressive-enhancement-still-pays-off\u002F","Progressive Enhancement Still Pays Off","Build the core experience in HTML first, then layer CSS and JavaScript so your site stays useful when networks, scripts, or devices misbehave.","2024-11-05",[92,11,269],"architecture",1784620505017]