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
--prodput 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:
- 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。
- Credits are not only consumed by deploys(See "What else consumes credits" below).
- 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--prodmust 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-setupand01-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
PATHto 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:
- First go tospamand take a look
- Wait 5 minutes (some school email has delayed quarantine)
- 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:
- 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
- Deployment has an API rate limit (3 times per minute), thirty people pressing at the same time → a whole bunch
429 Too Many Requests - 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。
💡
--jsonis for the agent to read: it prints the site information as JSON, and the agent can directly extractnameandurl, 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:setwill 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.appthis 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-toolkitbefore; 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
- 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.
- Bad example using draft-only deployment(0 points), don't
--prod。 - 🔴 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) |
💡
.mtsalso works (TypeScript version).Teachers should just use.mjsis fine, no need to learn TypeScript.
3-3 Complete minimal example (three files, ready to use)
✅ All three code snippets below pass
node --checksyntax 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.errorThe 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 sendupstream.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
modelname. 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-apionce 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.htmlthere 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 linklinked(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)
- Netlify dashboard → click your site
- Project configuration
- On the left select Environment variables
- 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:
- 「The same feature, the exact same screen.」
- 「That key the whole class just read aloud together, now no matter how hard you press, you can't find it.」
- 「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、備份.htmlinside 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, LineNumberIf 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_REPOorNL_NO_KEY_IN_BROWSERIf either isFAIL,NETLIFY_PACK_STATUSthen it must beFAIL。 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_PRODmust not be executed without the user's explicit consent, and when not executed, fill inSKIP, 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
--prodpush 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.