{"id":"l-rf-4-3","courseId":"react-fundamentals","lang":"en","kind":"lesson","summary":"Deploy the Watchlist to Vercel from a GitHub repository or the Vercel CLI, understand production and preview deployments, and handle environment variables and client-side routing safely.","html":"<p>A project that only runs on <code dir=\"ltr\">localhost:5173</code> is invisible to everyone else, including the people deciding whether to hire you. Deploying turns the Watchlist into a URL you can put on your CV, send to a friend and open on your phone. For a Vite app on Vercel this takes a few minutes, and once it’s set up, every <code dir=\"ltr\">git push</code> deploys automatically.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">What you’re deploying</h2>\n<p><code dir=\"ltr\">npm run build</code> turns your project into a folder of static files: <code dir=\"ltr\">dist/index.html</code>, one or two JavaScript bundles and a CSS file, with hashed file names like <code dir=\"ltr\">index-B3f9xk2a.js</code> so browsers can cache them forever. There’s no server code. Any static host can serve it; Vercel is popular because it detects the framework, builds on every push and gives you preview URLs for free on the Hobby plan.</p>\n<p>Before deploying, check the production build locally:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> build</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> preview</span></span></code></pre></div>\n<p><code dir=\"ltr\">preview</code> serves <code dir=\"ltr\">dist/</code> on <code dir=\"ltr\">http://localhost:4173</code>. If something breaks only in production, such as a wrong asset path or a missing environment variable, you find out here instead of on the live site.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Option 1: import from GitHub (recommended)</h2>\n<ol>\n<li>Push your project to a GitHub repository. If you committed in the Building the Watchlist lesson, create an empty repository on GitHub and follow its instructions to <code dir=\"ltr\">git remote add origin …</code> and <code dir=\"ltr\">git push -u origin main</code>.</li>\n<li>Sign in to Vercel with your GitHub account and choose <strong>Add New… → Project</strong>.</li>\n<li>Import the <code dir=\"ltr\">watchlist</code> repository. Vercel detects <strong>Vite</strong> and fills in the build command (<code dir=\"ltr\">vite build</code>, which is what <code dir=\"ltr\">npm run build</code> runs) and the output directory (<code dir=\"ltr\">dist</code>). Leave them as they are.</li>\n<li>Click <strong>Deploy</strong>. About a minute later you get a production URL like <code dir=\"ltr\">watchlist-yourname.vercel.app</code>.</li>\n</ol>\n<p>From now on, the repository drives deployments:</p>\n<ul>\n<li>Every push to <code dir=\"ltr\">main</code> builds and deploys to <strong>production</strong>.</li>\n<li>Every push to another branch, and every pull request, gets its own <strong>preview deployment</strong> with a unique URL, posted as a comment on the pull request.</li>\n</ul>\n<p>Preview deployments change how you work. You restyle the filter bar on a branch, open a pull request, and click the preview link on your phone before merging. Production never sees unfinished work.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-rf-4-3-t1\"><defs><marker id=\"arrow-l-rf-4-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-rf-4-3-t1\">A git push to GitHub triggers a Vercel build that runs vite build and produces dist. Pushes to main go to the production URL; pushes to other branches get a preview URL.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"80\" width=\"120\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"70\" y=\"113\" text-anchor=\"middle\">git push</text>\n  <rect class=\"d-box\" x=\"165\" y=\"80\" width=\"120\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"225\" y=\"113\" text-anchor=\"middle\">GitHub</text>\n  <rect class=\"d-box-primary\" x=\"320\" y=\"70\" width=\"160\" height=\"76\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"400\" y=\"100\" text-anchor=\"middle\">Vercel build</text>\n  <text class=\"d-code\" x=\"400\" y=\"124\" text-anchor=\"middle\">vite build → dist</text>\n  <rect class=\"d-box-success\" x=\"530\" y=\"30\" width=\"160\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"610\" y=\"54\" text-anchor=\"middle\">main → production</text>\n  <text class=\"d-label-muted\" x=\"610\" y=\"74\" text-anchor=\"middle\">your .vercel.app URL</text>\n  <rect class=\"d-box-accent\" x=\"530\" y=\"130\" width=\"160\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"610\" y=\"154\" text-anchor=\"middle\">branch → preview</text>\n  <text class=\"d-label-muted\" x=\"610\" y=\"174\" text-anchor=\"middle\">unique URL per push</text>\n  <path class=\"d-arrow\" d=\"M130 108 L163 108\" marker-end=\"url(#arrow-l-rf-4-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M285 108 L318 108\" marker-end=\"url(#arrow-l-rf-4-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M480 95 L528 62\" marker-end=\"url(#arrow-l-rf-4-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M480 122 L528 154\" marker-end=\"url(#arrow-l-rf-4-3-1)\"/>\n</svg><figcaption>From git push to a live URL</figcaption></figure>\n<h2 id=\"sec-3\" tabindex=\"-1\">Option 2: the Vercel CLI</h2>\n<p>If you’d rather deploy from the terminal, or the code isn’t on GitHub yet:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> install</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -g</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> vercel</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">vercel</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">          # first run: log in, link the project, deploy a preview</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">vercel</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --prod</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">   # deploy to production</span></span></code></pre></div>\n<p>The first <code dir=\"ltr\">vercel</code> run asks a few questions (scope, project name, which directory) and detects Vite the same way. It creates a <code dir=\"ltr\">.vercel</code> folder that links your directory to the project; keep it out of Git. The CLI is handy for quick experiments. For a project you’ll keep working on, the Git integration is better, because deployments happen without anyone remembering to run a command.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Environment variables</h2>\n<p>Sooner or later you’ll want configuration, such as an API base URL. Vite exposes environment variables to your code only if they start with <code dir=\"ltr\">VITE_</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> apiUrl</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">meta</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.env.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">VITE_API_URL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n<p>Locally, put them in a <code dir=\"ltr\">.env.local</code> file (already git-ignored by the Vite template). On Vercel, add them under <strong>Project Settings → Environment Variables</strong>, then redeploy, because Vite inlines the values when it builds.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Secrets in VITE_ variables</span></p>\n<p><code dir=\"ltr\">VITE_</code> variables are written into the JavaScript bundle as plain text. Anyone can open DevTools and read them. That’s fine for a public API URL and unacceptable for a private API key. Secrets belong on a server, such as a serverless function, never in client code.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">Routing on a static host</h2>\n<p>The Watchlist has one page, so every request is for <code dir=\"ltr\">/</code> and Vercel serves <code dir=\"ltr\">index.html</code>. If you later add client-side routing with React Router, a URL like <code dir=\"ltr\">/watched</code> exists only inside your JavaScript. Refreshing on it asks the server for a <code dir=\"ltr\">/watched</code> file that doesn’t exist, and you get a 404. The fix is a rewrite that serves the app shell for any path:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"json\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">vercel.json</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  \"rewrites\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [{ </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"source\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"/(.*)\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#116329;--shiki-dark:#7EE787\">\"destination\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"/index.html\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }]</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Real files in <code dir=\"ltr\">dist/</code>, such as your JavaScript and CSS, are still served normally; the rewrite only applies when no file matches.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Check the live site like a user</span></p>\n<p>Open the production URL on your phone, add a movie, refresh, and confirm it’s still there. localStorage is per site, so your localhost list and your production list are separate. That’s expected, not a bug.</p>\n</aside>\n<p>The Watchlist is live. In the last lesson, you’ll look back at what you’ve learned and plan what to learn next.</p>\n","toc":[{"id":"sec-1","text":"What you’re deploying","level":2},{"id":"sec-2","text":"Option 1: import from GitHub (recommended)","level":2},{"id":"sec-3","text":"Option 2: the Vercel CLI","level":2},{"id":"sec-4","text":"Environment variables","level":2},{"id":"sec-5","text":"Routing on a static host","level":2}],"takeaways":["A Vite app builds to static files in dist/, which Vercel detects automatically with the Vite preset (<code dir=\"ltr\">vite build</code>, output <code dir=\"ltr\">dist</code>).","Importing a GitHub repository gives you a production deployment on every push to main and a preview URL for every other branch and pull request.","The Vercel CLI deploys from your terminal; <code dir=\"ltr\">vercel</code> creates a preview and <code dir=\"ltr\">vercel --prod</code> deploys to production.","Only variables prefixed <code dir=\"ltr\">VITE_</code> reach browser code, and they are public, so never put secrets in them.","A single-page app with client-side routes needs a rewrite to index.html; the Watchlist, with one page, doesn’t."],"further":[{"title":"Vite on Vercel","url":"https://vercel.com/docs/frameworks/frontend/vite"},{"title":"Vercel CLI overview","url":"https://vercel.com/docs/cli"},{"title":"Deploying a Static Site (Vite)","url":"https://vite.dev/guide/static-deploy"},{"title":"Env Variables and Modes (Vite)","url":"https://vite.dev/guide/env-and-mode"}],"quiz":[{"q":"<p>You import the Watchlist repository into Vercel. What does Vercel run, and what does it serve?</p>","options":[{"text":"It runs <code dir=\"ltr\">npm run dev</code> and keeps the dev server running for visitors.","why":"The dev server is for local development only. Production serves optimized static files."},{"text":"It installs dependencies, runs the build (<code dir=\"ltr\">vite build</code>), and serves the static files in <code dir=\"ltr\">dist/</code>.","why":"Correct. The Vite preset knows the build command and output folder, so the defaults just work."},{"text":"It uploads your <code dir=\"ltr\">src/</code> folder and the browser compiles the JSX.","why":"Browsers can’t run JSX. The build step compiles and bundles everything before anything is served."},{"text":"It serves <code dir=\"ltr\">index.html</code> from the project root with no build step.","why":"That root index.html points at <code dir=\"ltr\">/src/main.jsx</code>, which only works through Vite. The built <code dir=\"ltr\">dist/index.html</code> is what gets served."}],"answer":1},{"q":"<p>You open a pull request that restyles the filter bar. With the GitHub integration set up, what happens?</p>","options":[{"text":"Nothing until you merge, because Vercel only builds main.","why":"Vercel builds every pushed branch. Only the production URL waits for main."},{"text":"The production site updates immediately with the unmerged changes.","why":"Production is only updated from the production branch (main), so unfinished work doesn’t reach real users."},{"text":"Vercel asks you to run <code dir=\"ltr\">vercel --prod</code> locally.","why":"The Git integration deploys automatically; the CLI is an alternative, not a required step."},{"text":"Vercel builds the branch and posts a unique preview URL on the pull request, leaving production unchanged.","why":"Correct. Preview deployments let you and reviewers click through the change before merging."}],"answer":3},{"q":"<p>Your app reads <code dir=\"ltr\">import.meta.env.VITE_TMDB_KEY</code> to call a movie API. Which statement is true?</p>","options":[{"text":"The value is embedded in the JavaScript bundle at build time, so anyone can read it in their browser.","why":"Correct. VITE_ variables are public by design. Only use them for values that are safe to expose, and keep real secrets on a server."},{"text":"The value stays on Vercel’s servers and is fetched securely at runtime.","why":"Vite replaces <code dir=\"ltr\">import.meta.env.VITE_…</code> with the literal value during the build. There’s no runtime fetch."},{"text":"The prefix is optional; any environment variable is available as <code dir=\"ltr\">import.meta.env.NAME</code>.","why":"Vite only exposes variables with the <code dir=\"ltr\">VITE_</code> prefix to client code, precisely to prevent accidental leaks."}],"answer":0},{"q":"<p>You add React Router so <code dir=\"ltr\">/watched</code> is its own page. Clicking around works, but refreshing on <code dir=\"ltr\">/watched</code> in production shows a 404. What’s the fix?</p>","options":[{"text":"Move the page into a <code dir=\"ltr\">public/watched/</code> folder.","why":"The page is rendered by JavaScript from index.html; a static folder would need its own copy of the whole app."},{"text":"Switch the build to <code dir=\"ltr\">npm run dev</code> on Vercel.","why":"The dev server isn’t a production server. The fix is a routing rule for the static files."},{"text":"Add a rewrite in <code dir=\"ltr\">vercel.json</code> that sends every path to <code dir=\"ltr\">/index.html</code>, so the app loads and the router handles the URL.","why":"Correct. There’s no <code dir=\"ltr\">watched</code> file on the server; the rewrite serves the app shell for any path and lets client-side routing take over."}],"answer":2}],"exercise":{"kind":"order","items":["build-preview","commit","push","import","deploy","branch-preview","merge"],"title":"Ship the Watchlist","prompt":"<p>Put these steps in order: from a working app on your laptop to a live production site, then shipping one more change safely through a preview deployment.</p>\n","hints":["Check the production build on your own machine before anything leaves it.","Vercel imports a repository, so the code has to be on GitHub first.","A change on a branch gets a preview URL; production only changes when main does."],"explanation":"<p>You verify the real build locally with <code dir=\"ltr\">npm run build</code> and <code dir=\"ltr\">npm run preview</code>, commit, and push to GitHub so Vercel has a repository to import. Importing detects Vite (<code dir=\"ltr\">vite build</code>, output <code dir=\"ltr\">dist</code>), and the first deploy gives you a production URL. After that, changes go on a branch: the pull request gets its own preview URL to check, and merging into <code dir=\"ltr\">main</code> triggers the production deployment.</p>\n","checkLabels":{},"itemText":{"build-preview":"Run <code dir=\"ltr\">npm run build</code> and <code dir=\"ltr\">npm run preview</code> and click through the app locally","commit":"Commit the working app with Git","push":"Create a GitHub repository and <code dir=\"ltr\">git push -u origin main</code>","import":"In Vercel, Add New → Project and import the repository (preset: Vite, output: dist)","deploy":"Click Deploy and open the production URL on your phone","branch-preview":"Restyle the filter bar on a branch, open a pull request and check its preview URL","merge":"Merge the pull request into main so production updates"}},"words":712,"readMinutes":3}