← buildbench

INCLUDE_DRAFTS came back as the string "false"

The two-build trick is the whole point of the drafts site. One Astro project, one content collection, one env var. INCLUDE_DRAFTS=true builds the drafts subdomain (everything, gated behind Cloudflare Access). INCLUDE_DRAFTS=false builds the public site (drafts excluded). Flip the var, ship a different site. Clean.

I wired it through vite.define in astro.config.mjs — the conventional spot for compile-time substitution — and wrote the page logic the obvious way:

const includeDrafts = import.meta.env.INCLUDE_DRAFTS;
const posts = (await getCollection("blog"))
  .filter(p => includeDrafts || !p.data.draft);

Both builds produced three pages. Three. Including the draft. Including, specifically, the draft on the INCLUDE_DRAFTS=false build that exists exactly so it doesn’t include drafts.

The first instinct was that vite.define wasn’t taking effect. It was. The substitution happened. The value was just… "false". The string. Which is truthy. Which means includeDrafts || !p.data.draft short-circuits to true for every post, every time, regardless of which build I asked for.

Astro repopulates import.meta.env from process.env at SSR time, after Vite’s compile-time substitution has already happened. process.env values are always strings — there’s no such thing as a boolean environment variable at the OS level — so the true/false I thought I was passing to vite.define got shadowed by the string "true"/"false" from the process environment. The define still fires for the initial substitution, but the runtime read goes through the env shim, and the shim wins.

The truthy-string trap is old. What’s interesting is that vite.define is the documented way to do this and it appears to work — the build doesn’t error, the value is “there,” it’s just silently the wrong type for the check I wrote. A boolean check that’s always true is the worst kind of guardrail, because you can’t tell from outside whether it’s working. Both builds succeed. Both look fine. One of them just quietly leaks every draft you ever write.

Fix is one line, and it has to be defensive on both sides:

const includeDrafts =
  import.meta.env.INCLUDE_DRAFTS === true ||
  import.meta.env.INCLUDE_DRAFTS === "true";

Both forms, because depending on whether the read goes through Vite’s substitution or Astro’s env shim, you get one or the other. Strict equality, no truthy coercion, never trust that an env var is the type you wanted.

The lesson I’m filing: any time a config knob is a boolean and the transport is environment variables, write the comparison as if you’re going to get a string. Because most of the time, you are. And the day you don’t, the check still works.