[{"data":1,"prerenderedAt":58},["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.","2026-07-18",[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,24,33,41,43,51],{"_path":16,"title":17,"description":18,"date":19,"tags":20,"readingTime":12},"\u002Fblog\u002Fwhy-indie-games-are-breaking-through\u002F","Why Indie Games Are Breaking Through","Small teams are outselling big studios on their best weeks. Here is what actually changed: distribution, community, and what players reward now.","2026-08-07",[21,22,23],"gaming","indie","culture",{"_path":25,"title":26,"description":27,"date":28,"tags":29,"readingTime":32},"\u002Fblog\u002Fpage-transitions-that-dont-fight-your-data\u002F","Page Transitions That Dont Fight Your Data","Route animations feel polished when they respect loading time, preserve context, and get out of the way for people who prefer less motion.","2026-07-30",[11,30,31],"vue","ux",6,{"_path":34,"title":35,"description":36,"date":37,"tags":38,"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",[39,11,40],"privacy","tools",{"_path":4,"title":5,"description":6,"date":7,"tags":42,"readingTime":12},[9,10,11],{"_path":44,"title":45,"description":46,"date":47,"tags":48,"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-07-15",[49,50,11],"accessibility","html",{"_path":52,"title":53,"description":54,"date":55,"tags":56,"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-07-07",[9,57,10],"ssg",1786156741315]