# robots Must Match Access Intent

**MUST** · **ID:** `design-robots-intent` · **Category:** design
**Source:** [@Ibelick](https://www.ui-skills.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-robots-intent

> MUST: The `robots` meta must match actual access intent, so DERIVE it from the environment instead of hardcoding: default to `noindex` and let exactly one environment opt in (`process.env.VERCEL_ENV === "production"`). Preview and staging deploys are public URLs nobody thinks of as public, and an indexed staging copy competes with production for your own brand name. The symmetric failure is the overcorrection: a hardcoded `noindex` merges and production silently deindexes itself — deindexing is quiet, traffic just stops. Reserve `noindex` for private, duplicate, or non-public pages.

Derive the robots tag from the deploy environment: noindex by default on previews and staging, index only where the page is genuinely public

From the canonical and indexing rules in ibelick's fixing-metadata skill. Preview deploys are public URLs that nobody thinks of as public. Leave them indexable and a crawler will eventually find one, at which point a half-finished staging copy of your site is competing with production for your own brand name — and because it is thinner, newer, and often a near-duplicate, the outcome is unpredictable in exactly the way you cannot afford. That is the expensive, common failure. The symmetric failure is what people ship when they overcorrect: someone hardcodes `<meta name="robots" content="noindex">` to fix staging, it merges, and production quietly deindexes itself. Nobody notices for weeks, because deindexing is silent — traffic just stops. Both failures come from treating robots as a static tag rather than a function of the environment. Derive it: default to noindex and let exactly one environment opt in (`process.env.VERCEL_ENV === 'production'`). Defaulting to noindex means every new preview surface is safe on the day it ships, and the single env that opts in is the one place you can actually check.

## Rule snippet

```tsx
const isProd = process.env.VERCEL_ENV === "production";
<meta name="robots" content={isProd ? "index,follow" : "noindex,nofollow"} />
```

## Bad — do not do this

`design-robots-intent-bad`

```tsx
export function RobotsIntentBad() {
  return (
    <div className="w-full space-y-4">
      {/* Failure 1: preview deploy is indexable */}
      <div className="rounded-lg border border-border bg-card p-3 space-y-1">
        <p className="mb-1 text-xs text-muted-foreground">Search: “myapp”</p>
        <div className="rounded-md border border-destructive p-2">
          <p className="text-xs text-muted-foreground">myapp-git-feat-x-acme.vercel.app</p>
          <p className="text-sm font-medium text-primary">MyApp (staging)</p>
        </div>
        <div className="rounded-md border border-border p-2">
          <p className="text-xs text-muted-foreground">myapp.com</p>
          <p className="text-sm font-medium text-primary">MyApp — Ship faster</p>
        </div>
        <p className="text-xs text-error">
          Every preview deploy is indexable, so your staging copy competes with production for your
          own brand name — and sometimes wins.
        </p>
      </div>

      {/* Failure 2: the overcorrection */}
      <div className="rounded-lg border border-border bg-card p-3 space-y-1">
        <p className="mb-1 text-xs text-muted-foreground">Search: “myapp” — after someone “fixed” it</p>
        <div className="rounded-md border border-dashed border-border p-3">
          <p className="text-xs text-muted-foreground">No results for myapp.com</p>
        </div>
        <p className="text-xs text-error">
          The symmetric failure: a hardcoded noindex shipped to production and quietly deindexed the
          whole site.
        </p>
      </div>

      <pre className="overflow-x-auto rounded-lg bg-muted p-3 text-xs text-foreground"><code>{`// preview: nothing at all — indexable
// "fix": <meta name="robots" content="noindex" />  ← now in prod too`}</code></pre>
    </div>
  );
}
```

## Good — do this

`design-robots-intent-good`

```tsx
const ENVIRONMENTS = [
  { env: 'production', tag: 'index, follow', indexable: true },
  { env: 'preview', tag: 'noindex', indexable: false },
  { env: 'development', tag: 'noindex', indexable: false },
];

export function RobotsIntentGood() {
  return (
    <div className="w-full space-y-4">
      <div className="rounded-lg border border-border bg-card p-3 space-y-2">
        <p className="text-xs font-semibold text-foreground">
          One gate, resolved per environment
        </p>
        <table className="w-full text-xs">
          <thead>
            <tr className="text-left text-muted-foreground">
              <th scope="col" className="pb-1 font-medium">Deploy</th>
              <th scope="col" className="pb-1 font-medium">robots</th>
              <th scope="col" className="pb-1 font-medium">In search?</th>
            </tr>
          </thead>
          <tbody>
            {ENVIRONMENTS.map(({ env, tag, indexable }) => (
              <tr key={env} className="border-t border-border">
                <td className="py-1 text-foreground">{env}</td>
                <td className="py-1 font-mono text-muted-foreground">{tag}</td>
                <td className={`py-1 font-medium ${indexable ? 'text-success' : 'text-muted-foreground'}`}>
                  {indexable ? 'Yes' : 'No'}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <pre className="overflow-x-auto rounded-lg bg-muted p-3 text-xs text-foreground"><code>{`const isProduction = process.env.VERCEL_ENV === 'production';

// noindex is the default; production is the one env that opts in.
{!isProduction && <meta name="robots" content="noindex" />}`}</code></pre>

      <p className="text-xs text-success">
        The tag is derived from the deploy, never hardcoded, so it always matches actual access
        intent. Defaulting to noindex means a new preview surface is safe on the day it ships, and
        the one env that opts in is the one you can check.
      </p>
    </div>
  );
}
```

## References

- [fixing-metadata (skill source)](https://raw.githubusercontent.com/ibelick/ui-skills/main/skills/fixing-metadata/SKILL.md)
- [Google: Block search indexing with noindex](https://developers.google.com/search/docs/crawling-indexing/block-indexing)
