SuperAPI AI Directory

opencode-netlify

Coding & development

View repository

Updated 10-15 23:58

About this skill

Deploying web pages and hiding keys (Netlify)

Official documentation verification date:2026-07-27 Basis:https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/credit-based-pricing-plans/、https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/how-credits-work/、https://docs.netlify.com/build/functions/get-started/、https://docs.netlify.com/build/environment-variables/overview/、https://docs.netlify.com/api-and-cli-guides/cli-guides/get-started-with-cli/、https://docs.netlify.com/api/get-started/、https://docs.netlify.com/build/build-with-ai/netlify-mcp-server/、https://vercel.com/docs/limits/fair-use-guidelines、https://vercel.com/docs/cli/deploy

⚠️ Quota and pricing figures are the state on the verification date,providers may adjust them at any time; the official page prevails。


🔴 First thing: credit discipline (skip this section and everything after it is wasted)

Netlify from 2025-09-04 onward changed to acredit system。New accounts registered after this date all enter this plan(teachers registering on-site at the workshop, 100% of them).

Fact Figure
Free plan monthly credits 300 credits
Is this a hard limit 🔴 Yes. The free plan cannot purchase add-ons and cannot auto-recharge
One production deploy(netlify deploy --prod) deducts 15 credits
So how many production deploys per month at most 🔴 20 times(and this is the theoretical upper limit; see below)
What happens when credits run out 🔴 All your websites go offline. Visitors opening the URL see Site not available
When will it come back Wait for the next billing cycle. There is no other way, and you cannot ask for help

🔴 And these do not use credits at all (0 credits)

Action Credits
Draft deploy(netlify deploy,no charge --prod) 0 credits
Deploy Preview, branch deploy 0 credits
Failed deploy 0 credits
Rollback(revert the production site to the previous version) 0 credits
Local preview (netlify dev) 0 credits(nothing was uploaded at all)

🔴 So this package has only one core rule

Draft first; only publish officially after confirming there are no problems.

You canunlimited timesdraft deploy, see exactly the same screen, click exactly the same buttons, and it costs nothing at all。

Every time you say, "Let's just --prod put it up and see," you are throwing away 1/20 of your monthly quota. Changing the layout five times in one afternoon = 75 credits = a quarter of the month is gone。

At the workshop, please say it directly like this:

  • netlify deploy = Test print. Free, unlimited times, and no pain if it prints badly.
  • netlify deploy --prod = Send to print. Only 20 sheets of paper per month.

⚠️ "20 times" is the theoretical value; in practice aim for 15–18 times

Three reasons:

  1. Production deploys may be blocked before credits reach zero. There are multiple reports in the user community: with 26–30 credits still showing on the account, production deploys were already rejected. The official documentation does not explain what the reserve threshold is, sodo not plan to use it down to the last credit。
  2. Credits are not only consumed by deploys(See "What else consumes credits" below).
  3. A deployment that fails because it hits the API rate limit costs 0 credits, butyou won't be able to resist retrying, and the successful retry gets charged.

🔴 Hard rules for AI Agents: Unless the user explicitly says in this turn "officially publish", "go live", or "I'm about to print the QR Code", always do a draft deployment only. Do not add --prodon your own just to "verify it while you're at it". Every --prod must be approved by the user first.

What else consumes credits

Besides official deployments, these also draw from the same 300 credits:

Item Rate Will teachers run into this?
Website traffic (bandwidth) 20 credits/GB ⚠️ Yes, if you put videos or large images. A 100MB video watched by 30 students = 3GB = 60 credits = 4 official deployments
Number of web requests 2 credits/10,000 requests ✅ The volume for one class is negligible
Function compute 10 credits/GB-hour ✅ The Q&A volume for one class is negligible
AI Gateway 180 credits ≈ 1 USD of AI usage 🔴 Draws from the same pool, directly crowding out deployment quota

🔴 Don't put videos or large images into the Netlify site. Put them on YouTube or Google Drive, and only put links on the webpage. This is the most easily overlooked item in this pack, but the one that most secretly drains your credits.

📌 AI Gateway is not recommended as the main path. It is a service where Netlify proxies AI API calls for you. The advantage is not needing to apply for a key yourself, but the disadvantage is thatevery cent spent is deducted from your deployment quota. Step three of this pack teaches "your own key + your own function". With this path, AI costs are billed by the AI service provider and won't bite into whether you can publish webpages this month.


Where this pack fits in the workshop (two uses)

This pack will appear in the workshoptwice, because it is two things at once:

Use A (second day morning): the third channel of "connecting external tools"

You have already gone through the first two:

Channel Represents What the agent gets
① MCP 03-notebooklm、04-obsidian Read and write someone else's service
② API 09-groq-api Call someone else's compute
③ CLI This pack Let the agent push what you made to a public URL

The first two are "bringing outside things in",while this one is sending your things out. After you finish, that index.html on your desk becomes aURL that the whole world can open, and you can print it as a QR Code and stick it on the back wall of the classroom.

Use B (third day afternoon): the backend for Work ⑥

By the third day, you will have an API key in hand (AI, subtitles, translation...). Keys cannot be written into the webpage — writing it in is equivalent to announcing it publicly; anyone can press F12 and copy it, then use your quota.

Netlify Functions is the solution:Keep the key on your own server, and the frontend only calls your own backend.

❌ 壞的做法
   學生的瀏覽器 ──(帶著你的金鑰)──→ 外部 AI API
                    ↑ 金鑰在這裡,按 F12 就看得到

✅ 本包的做法
   學生的瀏覽器 ──(不帶金鑰)──→ 你的 Netlify 函式 ──(帶金鑰)──→ 外部 AI API
                                      ↑ 金鑰只存在這裡,前端永遠碰不到

💡 Two usesshare the same Netlify account and the same set of commands. After finishing A on the second day, on the third day you can justadd functions to the same site—no need to start a new one.


What this package covers and what it doesn't

Covers

  • Push the web pages in a folder to https://<你的站名>.netlify.app
  • Make that URLstay the same even after redeployment(so you can print a QR Code with confidence)
  • Use Netlify Functions to create "your own backend" and hide the API key inside it
  • Teach you how toprove to the teacher on the spotthat the key is really hidden (compare before and after with F12)

Doesn't cover

  • Applying for an AI/subtitle service key (09-groq-api 's scope). This package assumes youalready have one OpenAI-compatible key
  • Databases, multi-user real-time sync → 06-supabase
  • Custom domains (xxx.edu.tw), SSL certificate setup
  • Frontend frameworks, bundlers (React/Vite/Next.js). This package only handlesplain HTML folders

📌 and 07-github(GitHub Pages)—what's the difference? GitHub Pages can only hoststatic files, with no backend, soit can't hide a key. Need to hide a key → this package. Just hosting a webpage and not touching keys → GitHub Pages works too, and there's no points issue.


Prerequisites

  • Completed 00-env-setup and 01-connect-model(OpenCode must work and be connected to a model)
  • Have a folder containing at least one index.html
  • Anemail address you can actually receive mail at(see the warning in step 1-4)
  • [Only required for use case B] Have an API key

Verify:

node --version

If you can see the version number, keep going. If not → go back and do 00-env-setup。


Step 1: Install tools, open accounts

📌 In the workshop flow, this step was alreadyon the first afternoondone along with 00-env-setup . But this package needs to be usable on its own, so the whole section is written here.If you've already installed it, skip straight to 1-5.

1-1 Node.js version must be new enough

Netlify's official documentation says "Node 18.14 or above,"but that number is already outdated。 netlify-cli the package's own engines field requires:

Node.js 22.13.0 or above

Go by the package's requirement, because what blocks you is it, not the documentation.

node --version
Seeing What to do
v22.13.0 or above ✅ Pass
Older than that (for example v20.x、v18.x) ❌ Must upgrade. Go back to 00-env-setup reinstall Node.js LTS
Nothing at all ❌ Node.js isn't installed. Go back to 00-env-setup

⚠️ The error message when the version is too low looks like this, and many people can't understand it: The engine "node" is incompatible with this module. Expected version ">=22.13.0" This isn't because you did something wrong, it's just that Node is too old.

1-2 Install Netlify CLI

npm install -g netlify-cli

After installingfirst close the terminal and open a new one, then verify:

netlify --version

🔴 "Reopen the terminal" is not just a polite formality. A freshly installed global command has to wait for PATH to update, anda window that is already open won't update itself. "It's clearly installed but it says the command can't be found" is almost always this.Just reopen it first.

1-3 🔴 Windows will get stuck here: PowerShell execution policy

After installing, type netlify, and Windows will very likely spit out this:

netlify.ps1 cannot be loaded because running scripts is disabled on this system.

This isn't because you installed it wrong. npm -g The command installed by .ps1 is ascript file on Windows, and by default PowerShell does not allow any script files to run。

Solution (just copy this line and paste it):

Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
Why use this line Explanation
-Scope Process Only affects your current terminal window. Close it and it returns to normal; it won't change your computer settings
No administrator privileges required 🔴 This is the key point. School computers often don't give teachers administrator privileges, so changing system-level settings gets stuck immediately
RemoteSigned Allows local scripts to run; scripts downloaded from the internet still require a signature. It's enough and not overly permissive

⚠️ You have to type it again every time you open a new terminal window. This is -Scope Process 's price, and also why it's safe. Finding it annoying and want to make it permanent?Please don't do that at the workshop. That changes the user-level security settings, so teachers can decide for themselves back at school; don't relax the computer's protections in class with thirty people.

Verify:

netlify --version

Printing the version number = passed.

💡 macOS/Linux doesn't have this problem, so just skip 1-3.

1-4 Register a Netlify account

🖐️ Do it all manually in the browser. The AI agent should not register on your behalf, should not fill in passwords for you, and should not receive verification emails for you.

Open https://app.netlify.com/signup。

Item Explanation
Do you need a credit card ✅ No. The free plan doesn't require linking a card (and precisely because of that, the credits are a hard cap, see the top)
Registration method Logging in with GitHub is the easiest (07-github that account), or you can use Email
Do you need to verify your email Yes. Without verification, CLI login will fail

⚠️ School email may not receive the verification email

School Google Workspace / education network emailoften blocks verification emails from foreign services, andwon't bounce, won't go to the trash, it just disappears.

Teachers get stuck at "I clearly registered" without any idea what happened.

Handling order at the workshop:

  1. First go tospamand take a look
  2. Wait 5 minutes (some school email has delayed quarantine)
  3. Still nothing → 🔴 Just switch to registering again with your personal Gmail; don't waste time with the school network admin. A workshop gets stuck waiting for the network admin to approve it, and the whole class just stops here.

🔴 One account per person

Don't have the whole class share one account just to save trouble. Three reasons:

  1. The 300 credits are shared across the entire account. Thirty people splitting 300 credits means 10 credits per person,not even enough for one official deployment
  2. Deployment has an API rate limit (3 times per minute), thirty people pressing at the same time → a whole bunch 429 Too Many Requests
  3. Site names are globally unique and will collide with each other

1-5 Let the CLI log in

netlify login

The browser will pop up an authorization page.

🖐️ Confirm that the account in the upper right corner is your own before clicking Authorize. When multiple accounts are logged in in the browser at the same time, it is very easy to authorize to someone else's account—especially in classrooms with shared computers.

Verify:

netlify status

If you can see your Email and account name = you're good.

What being stuck looks like Solution
The browser doesn't pop up The terminal will print a URL,copy and paste it into the browser yourself
After authorization the CLI is still spinning The school network blocks the callback. Try once with a phone hotspot
Not logged in Run again netlify login; if it still doesn't work, first netlify logout then log in

Step two [Use case A] Push the webpage to a public URL

Suppose you have a folder containing index.html(02-file-toolkit or made with OpenCode, either is fine).

2-1 Create a site

In that folderrun:

netlify sites:create --name class-demo-wang01 --json

🔴 How to choose a site name (there are two pitfalls here)

Rule Description
Can only uselowercase English letters, numbers, hyphens Chinese, spaces, and underscores are not allowed
🔴 Globally unique Not unique within your account, butunique across all of Netlify

So class-demo、test、my-site a name like thishas definitely already been taken by someone else, and it will throw:

name is taken

Naming rule at the workshop site: after itadd your own code。 class-demo-wang01、math7-lin-0727—it doesn't matter if it sounds bad,not colliding is more important。

💡 --json is for the agent to read: it prints the site information as JSON, and the agent can directly extract name and url, without having to guess the terminal's text layout.If you type it yourself, you can omit it.

Confirm the folder and site are connected

netlify link --name class-demo-wang01
netlify status

netlify status If you can see the site name = connected.

🔴 Do not skip this step. If it is not connected, later netlify env:set will ask you back "which site do you want to set?", and in non-interactive mode the agent will just freeze on that question, and it looks like a crash.

2-2 Draft deployment (0 credits, unlimited times)

netlify deploy --dir=.

--dir=. means "treatthis current folderas the web root". If the webpage is placed in public/ then write --dir=public。

When it finishes running it will print a Website Draft URL, it looks like this:

https://68f1a2b3c4d5e6--class-demo-wang01.netlify.app

Open it and check it thoroughly: whether the text is misaligned, whether the buttons work, whether it looks right on a phone.

🔴 You can try this step as many times as you want, and it won't cost you a single point. Change one line → then netlify deploy → then look again. This is the correct way to work.

2-3 🔴 URL stability (determines whether the QR Code can be printed)

Draft URL Production URL
Appearance https://<一長串隨機字元>--<站名>.netlify.app https://<站名>.netlify.app
After redeploying 🔴 It's a brand-new one every time ✅ It never changes
Can a QR Code be printed 🔴 Absolutely not ✅ Yes
Points 0 points 15 points
🔴 A draft URL cannot be used to print a QR Code.

On your next deployment, that URL will point to the old version (it won't break, but it's frozen at the version you had at the time). The sheet of paper stuck on the back wall of the classroom will forever stay at last week's half-finished product.

The only one to print is https://<站名>.netlify.app this one.

What should I do if the site name is already a random string of characters?

If you didn't use --name when creating the site, Netlify will give you graceful-tartufo-8c9f71 a name like this. It doesn't matter for the QR Code (scanning it will still get you there), butit can't be read aloud, can't be written on the blackboard, and if students mistype it they can't get in。

It's recommended to use --name to choose a good name right when you create the site. If it's already been created with a random string, go to the backend and change it:

Netlify backend → select your site → Domain management → Options → Edit site name

After changing it, the URL immediately becomes the new one,and the old URL stops working—sorenaming must be done before printing the QR Code。

2-4 Production deployment (🔴 15 points, only do it after asking the user)

Once you've looked at the draft and confirmed there are no problems,only thenrun:

netlify deploy --prod --dir=.

🔴 For the AI Agent: this linemust ask the user first. Suggested wording: "The draft looks fine now. Do you want to publish it officially?This will use 15 points, and with the remaining quota this month you can still publish about N more times." Only run it after the user says "yes".You are not allowed to casually add --prod。

The Website URL printed out after it finishes running is thatpermanent URL。

2-5 Generate the QR Code

Nowis when you generate the QR Code (the URL is already fixed):

請用 02-file-toolkit 的工具,把 https://class-demo-wang01.netlify.app 產成 QR Code 圖檔

💡 It's okay if you haven't used 02-file-toolkit before; any online QR generator will do. The key point isthe URL must be the production one。

2-6 Verification (every item must be seen)

# What to do What passing looks like
1 Open the production URL The page displays normally, and the address bar has a lock icon (HTTPS)
2 Usea phone(on a different network) to open the same URL It opens the same way ← this is what proves it's truly public
3 Change one word → netlify deploy(draft) → open the draft URL You can see the change,But the official URL is still the old one
4 Then netlify deploy --prod → Refresh the official URL A change appears,but the URL doesn't change at all ← This is why you can print a QR code
5 Backend → Usage You can see how many credits have been used and how many are left

🔴 After completing item 5, take a look every time. This is the only brake in this package.

2-7 ⚠️ Fallback: Netlify Drop (for keeping up with progress, cannot print QR codes)

When a teacher gets stuck on installation or registration and can't keep up, use this to let themsee results first:

Open https://app.netlify.com/drop → Drag the entire folder in → 30 seconds later you get a URL.

Pros Cons
No registration, no installation, no login 🔴 An unclaimed site, along with its URL, disappears after about 1 hour
You have a visual in 30 seconds Can't update via CLI; to revise, you have to drag the whole package again
Teachers who get stuck won't be left behind 🔴 Absolutely must not print a QR Code—it'll be dead before class ends

💡 This is a tool for "helping them keep up with this lesson," not a tool for "making a finished project." After class, you still need to go back and finish steps one to two. Want to keep the site that Drop produced → register an account and **claim** it within that hour.


Step 3 [Use Case B] Use Functions to hide the key in the backend

3-1 🔥 Teaching segment (part 1): First, let everyone see the key with their own eyes

This segment is the soul of the whole afternoon. Skip it, and the technical details that follow are just "type the commands as shown."

Method: First make a version that isdeliberately written wrong, with the key written directly into the webpage.

<script>
  // ⚠️⚠️⚠️ 這是「錯誤示範」,等一下就要把它拆掉 ⚠️⚠️⚠️
  const API_KEY = "gsk_這裡是我的金鑰";

  async function ask(q) {
    const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${API_KEY}`
      },
      body: JSON.stringify({
        model: "llama-3.3-70b-versatile",
        messages: [{ role: "user", content: q }]
      })
    });
    return (await res.json()).choices[0].message.content;
  }
</script>

Deploy it with a draft (0 credits), then walk the whole class through these two things:

Capture method 1: View the source code (you'll definitely see it)

F12 → Sources tab → open index.html → The key is right there, in plain text。

Capture method 2: Look at the network request (more impactful)

F12 → Network tab → click submit once on the webpage → click that request → Headers → scroll down to find Request Headers → see this line:

authorization: Bearer gsk_這裡是我的金鑰

Project it, and have the whole class read it out loud together.

🔴 On site, please say this sentence in full: 「This isn't because I set it up poorly; it's the nature of frontend webpages. All the code of a webpage must be sent to the student's browser to run— soanything written into a webpage can be seen by the whole world, no exceptions. No amount of 'encrypt it a bit' or 'obfuscate it a bit' can solve this; that only makes it take three extra minutes to copy."

🔴 Three on-site disciplines

  1. Any key used for demonstration must be voided on the spot after use. It's already on the projector screen, in the recording, and in photos on thirty phones. After the demonstration ends, go to the provider's backendrevoke it and generate a new one。 🔴 Deleting the file or the deployment won't help. The key is already in someone else's hands; rotating it is the only effective remedy.
  2. Bad example using draft-only deployment(0 points), don't --prod。
  3. 🔴 The free plan has no key leak detection. Netlify's "Smart secret detection" is a feature only available on the Personal (paid) plan. In other words:If you hardcode a key in a webpage and deploy it, Netlify won't warn you, won't block you, won't email you. There's no safety net. That's why you have to rely on discipline.

3-2 Folder structure (🔴 put it in the wrong place and it just won't work)

你的專案資料夾/
├── public/                    ← 網頁放這裡(要公開的東西)
│   └── index.html
├── netlify/
│   └── functions/             ← 🔴 函式一定要在這個路徑
│       └── ask-ai.mjs         ← 🔴 副檔名一定要 .mjs
└── netlify.toml               ← 設定檔
Rule Why Symptom of misplacement
The function must be in netlify/functions/ This is the default path Netlify recognizes Deployment succeeds, but calling the function returns 404
Use the extension .mjs Only then can you use export default this kind of ES module syntax On deploy it reports Cannot use import statement outside a module
Put webpages in public/, not in the root directory This wayThe only thing made public is what's in public/ in it, so the function source code and config files won't get mixed in (It won't break, but the layout will have a bunch of files that shouldn't be public)

💡 .mts also works (TypeScript version).Teachers should just use .mjs is fine, no need to learn TypeScript.

3-3 Complete minimal example (three files, ready to use)

✅ All three code snippets below pass node --check syntax check.

File 1/3:netlify.toml

[build]
  publish = "public"
  functions = "netlify/functions"

This file's purpose is totell Netlify "where the webpages are, where the functions are", so that future deployments don't require typing a long string of parameters every time.

File 2/3:netlify/functions/ask-ai.mjs(🔴 The key is hidden here)

// netlify/functions/ask-ai.mjs
// 前端呼叫這支函式;金鑰只存在 Netlify 伺服器上,前端永遠拿不到。

export default async (req) => {
  const json = (obj, status = 200) =>
    new Response(JSON.stringify(obj), {
      status,
      headers: { "Content-Type": "application/json; charset=utf-8" }
    });

  if (req.method !== "POST") {
    return json({ error: "只接受 POST" }, 405);
  }

  // ① 從 Netlify 的環境變數拿金鑰(不是從前端拿)
  const apiKey = process.env.AI_API_KEY;
  if (!apiKey) {
    return json({ error: "伺服器還沒設定 AI_API_KEY" }, 500);
  }

  // ② 讀前端送來的問題
  let question = "";
  try {
    const body = await req.json();
    question = String(body.question ?? "").trim();
  } catch {
    return json({ error: "請求格式不對,要送 JSON" }, 400);
  }
  if (!question) {
    return json({ error: "問題是空的" }, 400);
  }
  if (question.length > 500) {
    return json({ error: "問題太長,請縮短到 500 字以內" }, 400);
  }

  // ③ 帶著金鑰去打外部 AI API
  const upstream = await fetch("https://api.groq.com/openai/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${apiKey}`
    },
    body: JSON.stringify({
      model: "llama-3.3-70b-versatile",
      messages: [
        { role: "system", content: "你是國中小老師的教學助理。請用繁體中文回答,200 字以內。" },
        { role: "user", content: question }
      ]
    })
  });

  if (!upstream.ok) {
    // 錯誤細節只寫進伺服器日誌,不回給前端
    console.error("上游 API 錯誤:", upstream.status, await upstream.text());
    return json({ error: "AI 服務暫時無法使用,請稍後再試" }, 502);
  }

  const data = await upstream.json();
  const answer = data.choices?.[0]?.message?.content ?? "(沒有拿到回覆)";

  return json({ answer });
};

// ④ 自訂網址:前端就用 fetch("/api/ask-ai") 呼叫
export const config = { path: "/api/ask-ai" };

The four key points of this code:

# Code Plain language
① process.env.AI_API_KEY The key is taken from the server's environment variable, not in the file, not on the front end
② req.json() Receives the question sent from the front end.Basic checks are done: empty or too-long ones are blocked
③ Authorization: Bearer ${apiKey} This line runs on the server, the student's browser can't see it
④ export const config = { path: "/api/ask-ai" } Give this function an easy-to-remember URL

🔴 console.error The purpose of that line: error messages from external APIssometimes spit back key fragments. So error details are only written to the server log (only you can see them), and the front end only receives "temporarily unavailable". Don't send upstream.text() straight back to the front end just for easier debugging—that would defeat the whole purpose.

💡 Switching to another API only requires changing two lines: the URL in ③, and the model name. As long as it's an OpenAI-compatible format service (Groq, OpenRouter, Together...), it's all the same setup. Applying for a key is outside the scope of this package(09-groq-api once written will cover it); having any key on hand is enough to do this section.

File 3/3:public/index.html

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>問問 AI</title>
</head>
<body>
  <h1>問問 AI</h1>
  <input id="question" placeholder="輸入你的問題" style="width:20em">
  <button id="send">送出</button>
  <pre id="answer" style="white-space:pre-wrap"></pre>

  <script>
    const btn = document.getElementById("send");
    const box = document.getElementById("question");
    const out = document.getElementById("answer");

    btn.addEventListener("click", async () => {
      const question = box.value.trim();
      if (!question) { out.textContent = "先輸入問題喔"; return; }

      btn.disabled = true;
      out.textContent = "思考中…";
      try {
        // ⬇️ 打的是「自己的網站」,不是外部 AI 服務。這裡沒有金鑰。
        const res = await fetch("/api/ask-ai", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ question })
        });
        const data = await res.json();
        out.textContent = data.answer ?? ("錯誤:" + (data.error ?? "未知"));
      } catch (err) {
        out.textContent = "連線失敗:" + err.message;
      } finally {
        btn.disabled = false;
      }
    });
  </script>
</body>
</html>

🔴 the whole index.html there is no key anywhere in it. That is the point. fetch("/api/ask-ai") the one at the start of / means “my own website”—— the student's browser only knows to ask your website,and has no idea which AI is behind it or which key is used.

3-4 Set environment variables (two ways, know both)

The key goes into Netlify,not into a file. Two paths, pick one.

Method A: One command (suitable for an agent to run)

netlify env:set AI_API_KEY "gsk_你的金鑰" --secret
Part Description
AI_API_KEY variable name, must match the one in the function process.env.AI_API_KEY exactly(case-sensitive too)
"..." 🔴 Wrap the key in double quotes. If you don't, keys containing special characters will be broken apart by PowerShell
--secret Mark it as a secret value; the dashboard will only show a masked version

🔴 You must run it in a folder that has been netlify link linked(step 2-1). If it is not linked, it will ask you which site you want to set it for,and the agent will get stuck on that question。

Verification:

netlify env:list

You can see the name AI_API_KEY (the value will be masked, which is normal) = pass.

Method B: Dashboard web page (four steps, easier for teachers to remember back at school)

  1. Netlify dashboard → click your site
  2. Project configuration
  3. On the left select Environment variables
  4. Add a variable → fill Key with AI_API_KEY, paste the key into Value → check Contains secret values → Create

💡 Demonstrate both at the workshop. Method A is fast and can be handed to an agent;Method B is the one teachers will actually use when they come back three months later to change the key—— because by then they have probably forgotten the CLI commands, but the dashboard buttons are still in the same place.

🔴 Good news: changing the key does not require redeploying

Netlify's official documentation states clearly:Environment variables in the Functions scope take effect immediately when changed and do not require redeployment.

Thisdirectly saves you 15 points. The key was typed wrong, the key needs to be changed, you switched AI providers—— just change the environment variable, no need for --prod。 the function will get the new value the next time it is called.

(For comparison:build-timeenvironment variables are the ones that require redeployment. This packet does not use that kind.)

3-5 Local testing (0 points, fix all the errors here first)

netlify dev

It will start at http://localhost:8888 an environment identical to the live one: web pages, functions, and environment variables are all there.

Test the functionality here until it works——submit a question, see whether there is an answer, see whether F12 shows any red text. Nothing is uploaded at all, and no points are deducted at all.

Press Ctrl + C to end.

What being stuck looks like Solution
Function returns 404 File location or extension is wrong → go back to 3-2 to compare
Back 伺服器還沒設定 AI_API_KEY The environment variable isn't set, orthe variable name is misspelled; or this folder doesn't have netlify link
return AI 服務暫時無法使用 The key is invalid / the quota is used up / the model name is wrong.Look at the console.error line in the terminal, the real reason is there
Even netlify dev won't start First netlify status confirm that the login and connection are both there

3-6 Deploy (draft first, then production)

netlify.toml has already written the path, so the command is very short:

netlify deploy

💡 Without netlify.toml , you have to pass the parameters yourself: netlify deploy --dir=public --functions=netlify/functions 🔴 Missing --functions, the function won't be uploaded, and online it will be a 404.

Open the draft URL,test it completely once(this time it's a real online environment, still different from local).

Confirm there are no problemsonly thenask the user whether to publish to production:

netlify deploy --prod

3-7 🔥 Teaching segment (part 2): press F12 again

This moment is the wrap-up of the whole afternoon.

Go back to the exact same action as in step 3-1:F12 → Network → send a question → click that request。

This time what you see
request URL https://<你的站名>.netlify.app/api/ask-ai ← is your own website
Request Headers 🔴 No Authorization. No key. Nothing at all.
Response Only one line {"answer":"..."}
Sources tab Search all of index.html,can't find any key

Please say these three sentences out loud:

  1. 「The same feature, the exact same screen.」
  2. 「That key the whole class just read aloud together, now no matter how hard you press, you can't find it.」
  3. 「Because it's on the server. The student's browser never received it.」

🔴 The before-and-after comparison is the soul of this section. If you only say "you need to use Functions to hide the key", the teacher nods but won't do it; first let him see the key running naked with his own eyes, then see it disappear with his own eyes, he will remember this for the rest of his life. Sodon't skip the bad example in 3-1 just to save time—that is what this lesson is really teaching.

3-8 Verification (you must see every item)

# Check What passing looks like
1 The webpage sends a question An answer appears
2 F12 → Network → Request Headers for that request No Authorization this item
3 F12 → Sources → index.html Search all text for your key Can't find it
4 netlify env:list Can see AI_API_KEY
5 Search the project folder's full text for the key string Not found in a single file(the key is only on Netlify)
6 Backend → Usage Confirm there are still enough credits

🔴 Item 5 must really be done. Common situation: when testing, the teacher first hardcodes the key in some file, and after changing it to an environment variableforgets to delete that line, or leaves it in .env、test.html、備份.html inside and then deploy it together.

PowerShell search method (run in the project folder):

Get-ChildItem -Recurse -File | Select-String -Pattern "gsk_|sk-" | Select-Object Path, LineNumber

If there are results, it hasn't been hidden well yet.


Why not use Vercel

Among similar services, Vercel is the most famous, but this packagedoes not take that route for the main line. Three reasons, and the first is the most important.

① 🔴 The free plan (Hobby) explicitly prohibits commercial use, and the definition of "commercial" steps on teachers

Vercel's Fair Use Guidelines state in black and white: the Hobby planis limited to non-commercial personal use. And its definition of "commercial use" includes—

any deployment that produces financial benefit for anyone involved in producing the project,including code written by salaried employees or consultants。

Teachers are salaried employees. A webpage you make during working hours for teachingliterally falls within this definition。

This isa gray area, not "you will definitely be suspended." But:

🔴 A workshop cannot put thirty teachers into a position that "the terms might not allow." Teachers can judge for themselves after returning to school; butwhat is taught from the stage must be clean。

Netlify's free plandoes not have this restriction—it is clearly usable for teaching purposes. This one point is enough to decide the main line.

② The CLI's first deployment is forced to be a production deployment

Vercel's official documentation states:The first deployment of a new project is always production, even if you do not add --prod。

In other words:You do not have the chance to "take a look first and then decide." And the core discipline of this entire package is "draft first, confirm, then make it official"—this route directly conflicts with it.

Netlify is the opposite:Without adding --prod it is always a draft, and you can look at it as many times as you want.

③ The official MCP is currently read-only

At this stage, Vercel MCPcan only read(check documentation, view projects, view deployment logs),it cannot deploy. So even if it is installed, the agent still has to use the CLI. Installing an extra layer yet having half the functionality missing.

Then when can Vercel be used

As a backup: a temporary exit when Netlify credits run out, or when Netlify service is interrupted.

🔴 But do not switch platforms for the same project. Switching platforms = switching URLs = QR Codes need to be reprinted = all the ones already posted become invalid. If you really need to switch,finish switching before printing the QR Code。


Why use the CLI instead of MCP

The previous packages (03-notebooklm、04-obsidian) all teach MCP, but this package tells you to type commands. The reason is very practical:

MCP This package: CLI
Is OpenCode on the official support list 🔴 Neither Netlify's nor Vercel's list includes OpenCode(they list Claude Code, Codex, Cursor, VS Code, Antigravity, etc.) No list is needed. The CLI is just a command, and anyone can run it
Does it require OAuth authorization Yes, andschool networks are very good at blocking callbacks No (netlify login Do it only once; after that the token is stored locally)
When it breaks "The agent says it doesn't have this tool" — troubleshooting it is painful The command prints the error message directly,paste it to the agent and it can fix it
Can the teacher understand it? Can't see what's happening Can see every command line and result, and this has value for teaching

🔴 "Not listed on the official support list" does not equal "definitely can't be used", but it also doesn't equal can be used. At the workshopdon't gamble on this kind of thing. The CLI is the path that is certain to work.

If you really want to try MCP (play with it at home, not at the workshop)

Method Setup
Remote (officially recommended) https://netlify-mcp.netlify.app/mcp
Local npx -y @netlify/mcp(requires Node 22 or above)

Write into ~/.config/opencode/opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "netlify": {
      "type": "remote",
      "url": "https://netlify-mcp.netlify.app/mcp",
      "enabled": true
    }
  }
}

🔴 be sure to test it yourself first: completely close OpenCode and reopen → opencode mcp list → say to it "list my Netlify sites". If it can list them, only then does it count as working.If it can't list them, go back to using the CLI; don't struggle with it the night before the workshop.

⚠️ And remember:MCP also deploys, and also deducts 15 points. Handing it to an agent to deploy in natural language feels great, but it may do a production deployment without you noticing. If you use MCP,point discipline must be emphasized even more strictly than with the CLI.


Common pitfalls

Symptom Cause / Solution
netlify.ps1 cannot be loaded because running scripts is disabled 🔴 PowerShell execution policy. Run Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned(see 1-3).Must be rerun in every new window
The engine "node" is incompatible... Expected ">=22.13.0" Node is too old. The 18.14 written in the official docs is outdated,go by the 22.13 required by the package. Go back to 00-env-setup upgrade
Clearly it's installed but it says netlify is not a recognized command PATH was not updated. 🔴 Close the terminal, open a new oneand try again
No verification email received after registration The school mailbox blocked it, and it won't bounce back. Check spam first → wait 5 minutes → just switch to a personal Gmail and register again, don't wait for the network admin
npm install gets stuck or times out School firewall. Ask the network admin to allow registry.npmjs.org
netlify login Browser doesn't respond / gets stuck after authorization School firewall. Ask the network admin to allow api.netlify.com; first test once with a phone hotspot to confirm it's a network issue
Deployment succeeds but students can't open the URL School firewall blocks *.netlify.app. Ask the network admin to allow (This one must be handled by the school in advance; there's no time on site)
name is taken Site names are globally unique. Add your own code:class-demo-wang01
429 Too Many Requests Deployment rate limit:3 times per minute, 100 times per day. Wait one minute. 🔴 Shared accounts will definitely run into this, one account per person
The QR Code content printed out is old 🔴 Printed todraft URL. The draft URL is different on every deployment, and it's frozen at that version. You can only print <站名>.netlify.app
changed the site name, and all the old QR Codes stopped working Renaming changes the URL.Renaming must be done before printing the QR
The Netlify Drop site disappeared Unclaimed sites are deleted after about 1 hour. This is by design, not a malfunction. If you want to keep it, register and claim it within that hour
Calling the function returns 404 ① The file isn't in netlify/functions/; ② the deployment didn't include --functions(or there's no netlify.toml);③ config.path and the frontend fetch paths don't match
Cannot use import statement outside a module The extension isn't .mjs. Just rename it
The function returns "the server hasn't set AI_API_KEY yet" ① The environment variable isn't set; ② the name is misspelled or has different capitalization; ③ this folder doesn't have netlify link
netlify env:set is stuck there and won't move The folder isn't linked to a site; it's waiting for you to choose. First netlify link --name <站名>
changed the key but it didn't take effect Environment variables in the Functions scope areeffective immediately, no redeployment needed. If it doesn't take effect, it's usually becauseyou changed a different site—use netlify status to confirm which site you're on
There are still 30 credits left, but the production deployment is blocked Known reports: it gets blocked before reaching zero; the official threshold hasn't been explained.Don't plan to use it down to the last credit, budget 15–18 times
The site suddenly shows everything as Site not available 🔴 Credits used up.The free plan can't buy more, you can only wait for the next billing cycle, or upgrade to a paid plan
Credits drop faster than expected Check whetherthe site has videos or large images(bandwidth 20 credits/GB), or whether you used AI Gateway(180 credits ≈ 1 USD)
accidentally deployed the key 🔴 Immediately go to the provider's backend, revoke that key, and generate a new one. deleting files, deleting deployments, switching to a private repo none of it works. And the free planwon't warn you(no secret detection)
want to chain multiple commands but get a syntax error PowerShell 5.1 doesn't support &&, use ;

🔒 Security red lines (three)

Red line one: keys can only exist in two places

✅ can: Netlify environment variables, your own password manager. ❌ cannot: web files, GitHub (not even a private repo), chat windows, slides,.env being deployed together.

Red line two: there's only one way to handle a leak—replace it

Deleting files, deleting deployments, switching to private, telling everyone "don't use it"—all ineffective。 🔴 Go to the provider's backend to revoke it and regenerate it. This is the only effective action.

And the free planhas no leak detection, so no one will remind you.You have to remember it yourself.

Red line three: Functions must not return upstream errors to the frontend verbatim

External API error messagessometimes spit back key fragments. In the example, use console.error is written to the server log, and only a single "temporarily unavailable" is returned to the frontend, precisely for this reason.


✅ Success signals (machine-readable)

Execute in order, and the agent judges according to the rules:

node --version                 # 要 >= 22.13.0
netlify --version              # 印得出版本號
netlify status                 # 要看得到帳號與站名
netlify env:list               # 要看得到金鑰的變數名稱(用途 B 才需要)
Check item Pass condition
NL_NODE node --version >= v22.13.0
NL_CLI netlify --version prints the version number (Windows requires execution policy to be lifted)
NL_LOGIN netlify status shows the logged-in account
NL_LINK netlify status shows the linked site name
NL_DRAFT netlify deploy succeeds, and the draft URL opens
NL_PROD netlify deploy --prod succeeds, and the production URL opens (must be executed only with user consent)
NL_URL_STABLE twice in a row --prod, and the production URL strings are completely identical
NL_FN_200 curl.exe -X POST <正式網址>/api/ask-ai -H "Content-Type: application/json" -d "{\"question\":\"測試\"}" returns HTTP 200 and contains answer
NL_ENV netlify env:list contains that variable name
NL_NO_KEY_IN_REPO Search the entire project folder for the key string,zero hits
NL_NO_KEY_IN_BROWSER F12 → Network request headersdo not contain Authorization
NL_CREDITS The remaining credits on the backend Usage page have been recorded and reported

Finally output this section (for anything not done, fill in SKIP, and for failures fill in FAIL:<原因>):

NETLIFY_PACK_STATUS=OK
NL_NODE=OK
NL_CLI=OK
NL_LOGIN=OK
NL_LINK=OK
NL_DRAFT=OK
NL_PROD=OK
NL_URL_STABLE=OK
NL_FN_200=OK
NL_ENV=OK
NL_NO_KEY_IN_REPO=OK
NL_NO_KEY_IN_BROWSER=OK
NL_CREDITS=OK

🔴 NL_NO_KEY_IN_REPO or NL_NO_KEY_IN_BROWSER If either is FAIL,NETLIFY_PACK_STATUS then it must be FAIL。 These two items are exactly the point of the entire use case B.Skipping is not allowed, and "continue first and make it up later" is not allowed.

🔴 NL_PROD must not be executed without the user's explicit consent, and when not executed, fill in SKIP, which does not count as a failure.


Completion report format

## Netlify 部署結果

### 點數(🔴 每次都要回報)
- 本次正式部署(--prod)次數:<N> 次 → 消耗 <N×15> 點
- 草稿部署次數:<N> 次 → 0 點
- 後台 Usage 顯示剩餘點數:<剩餘>/300
- 本月估計還能正式部署:<約 N 次>(建議抓 15~18 次上限,不要用到見底)

### 環境
- Node.js:<版本>(需 >= 22.13.0):PASS / FAIL
- netlify-cli:<版本>:PASS / FAIL
- Windows 執行原則已解除(本次視窗):是 / 否 / 非 Windows
- 帳號:<email> 一人一帳號:是 / 否

### 用途 A:網頁部署
- 站名:<站名>
- 正式網址:https://<站名>.netlify.app
- 草稿網址(🔴 不可印 QR):<url>
- 網址穩定性驗證(NL_URL_STABLE):PASS / FAIL
- QR Code 已產生:是 / 否 → 用的是**正式**網址:是 / 否

### 用途 B:Functions 藏金鑰
- 函式檔案:`netlify/functions/<名稱>.mjs`
- 對外路徑:`/api/<名稱>`
- 環境變數名稱:<KEY_NAME>(值未記錄,也不應記錄)
- 函式端對端測試(NL_FN_200):PASS / FAIL / SKIP
- 🔴 專案內全文搜尋金鑰(NL_NO_KEY_IN_REPO):PASS / FAIL
- 🔴 瀏覽器請求標頭無 Authorization(NL_NO_KEY_IN_BROWSER):PASS / FAIL
- F12 前後對比教學橋段已完成:是 / 否 / 不適用
- 示範用的金鑰已作廢並換新:是 / 否 / 未做示範

### 安全確認
- 已告知「免費方案沒有金鑰外洩偵測,不會警告你」:是 / 否
- 已告知「金鑰外洩唯一處置是撤銷重產」:是 / 否
- 站台內沒有影片、大圖等會吃頻寬點數的檔案:是 / 否

### 待處理
- <把 FAIL 的項目與原因列在這裡;全部 PASS 就寫「無」>

If it is messed up, how to start over

💡 Good news: starting over does not cost credits. Draft deploys, failed deploys, and rollbacks are all 0 credits.

What you want to redo Method Credits
This version of the production site is broken, and you want to roll back to the previous version Backend → Deploys → find the last good version → Publish deploy 0 credits
The folder is linked to the wrong site netlify unlink then netlify link --name <正確站名> 0 credits
The site name was badly chosen (QR not printed yet) Backend → Domain management → Options → Edit site name 0 credits
Environment variables were set incorrectly netlify env:unset <KEY> then reset.No need to redeploy 0 credits
The key leaked 🔴 Go to AI service providerbackend to revoke and regenerate → netlify env:set update → No need to redeploy 0 credits
You do not want the whole site anymore Backend → Project configuration → General → at the very bottom Delete project 0 credits
CLI login is broken netlify logout then netlify login 0 points

🔴 Rollback is 0 points, remember this. When an error is found on the live site,do not"hurry up and fix it then --prod push it up" (that's 15 points). First roll back to the previous version (0 points), save the screen, then slowly revise it in the draft until it's good, and only publish once at the end.


Next step

  • Web page needs to store data (registration, submission, roll call) → 05-sheets-gas
  • Web page needs multiple people writing at the same time (word cloud, buzzer quiz, real-time leaderboard) → 06-supabase
  • Project needs backup, needs to be recoverable when switching computers → 07-github
  • Need to turn the live URL into a QR Code → 02-file-toolkit

📌 This pack and 07-github 's division of labor: GitHub storessource code(recoverable if broken), Netlify handlesgoing live(outsiders can open it). Doing both is what makes it complete:code is backed up, the URL can be printed.