商用網站架設實務

LESSON-25 · checkpoint

頁面開發:服務、關於與聯絡頁

主要結構已經完成,只留下本堂一個核心缺口。先預測,再從指定檔案找出它。 這頁用 可觀察證據anchor 指出下一個動作。

Starter3 個小成果
Checkpoint一個核心缺口
Solution4 個差異

開始前確認

這一頁的結果:修正前 FormData 缺少 message

工作方式:先把這一階段複製到自己的工作資料夾,開啟 files/index.html,記下修改前的畫面、DOM、Console 或 Network狀態。

會用到的檔案:index.htmlstyle.cssmain.jsservices.htmlabout.htmlcontact.html

本堂焦點:用原生 HTMLCSSJavaScript 完成服務、關於與聯絡三頁

照這個順序做

一次只做一張卡。完成後先重新整理入口,再留下證據,才進入下一張。

CHECKPOINT-1

理解 id 是頁面定位,name 才會成為 FormData 的欄位名稱。

目的

理解 id 是頁面定位,name 才會成為 FormData 的欄位名稱。

修改檔案

contact.html

程式碼位置

message textarea

現在要做:請在 contact.html 的「message textarea」位置完成:只補上 name="message",不要改動其他欄位或 JavaScript

貼上位置:contact.html:message textarea 區塊,先保留同一檔案的其他內容。

<textarea id="message" name="message" required></textarea>

預期結果:修正前 payload 缺少 message;修正後 FormData 包含 name、email、message。

驗收證據:Elements 的 textarea attribute 與 Console payload 三個 key。

卡住時先看這裡

常見錯誤:新增第二個 textarea、改 id,或把 name 寫成 message-text。

第一個檢查:只對照 textarea 的 id=message 與 name=message。

階段檔案

完整檔案收在可展開的卡片中;先完成上方微步驟,再用這裡查閱與複製。檔案位置要和目前的相對路徑一致。

開啟 checkpoint 的可執行入口

index.html多頁網站首頁:提供四頁導覽與本階段的操作提示。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="LESSON-25 Checkpoint 多頁商用網站練習。">
  <title>LESSON-25 Checkpoint|安心維護</title>
  <link rel="icon" href="../../../assets/favicon.svg" type="image/svg+xml">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="site-header">
    <a class="brand" href="index.html">安心維護</a>
    <nav aria-label="主要導覽">
      <a href="index.html" aria-current="page">首頁</a>
      <a href="services.html">服務</a>
      <a href="about.html">關於</a>
      <a href="contact.html">聯絡</a>
    </nav>
  </header>
  <main class="page-shell">
    <p class="eyebrow">LESSON-25 · checkpoint</p>
    <h1>多頁商用網站</h1>
    <p class="page-description">Starter 的頁面內容已完成,現在只檢查表單資料契約。</p>
    <section class="content-section" aria-labelledby="next-title">
      <h2 id="next-title">下一個檢查點</h2>
      <p>開啟聯絡頁,確認需求欄位的 <code>id</code> 與 <code>name</code> 是否都存在。</p>
      <a class="button-link" href="contact.html">開啟聯絡頁</a>
    </section>
  </main>
  <footer class="site-footer"><p>Checkpoint:只修正一個欄位契約。</p></footer>
  <script src="main.js" defer></script>
</body>
</html>
style.css共用呈現層:提供服務卡、表單、focus 與 390px 窄版規則。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

:root { font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans TC", sans-serif; color: #17324d; background: #f5f8fb; }
* { box-sizing: border-box; }
body { min-width: 320px; min-height: 100vh; margin: 0; line-height: 1.7; }
a { color: #155e75; }
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 3px solid #f1ab2c; outline-offset: 3px; }
.site-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; width: min(100% - 2rem, 920px); margin: 1rem auto 0; padding: .75rem 0; }
.brand { color: #17324d; font-weight: 800; text-decoration: none; white-space: nowrap; }
.site-header nav { display: flex; flex-wrap: wrap; gap: .7rem 1rem; }
.site-header nav a[aria-current="page"] { font-weight: 800; text-decoration-thickness: 3px; text-underline-offset: .25rem; }
.page-shell { width: min(100% - 2rem, 760px); margin: 2rem auto; padding: clamp(1.25rem, 4vw, 2rem); border: 1px solid #cbd8e4; border-radius: 20px; background: #fff; }
.eyebrow { margin: 0 0 .5rem; color: #176b87; font-weight: 800; letter-spacing: .06em; }
h1, h2, h3 { line-height: 1.25; }
h1 { margin: 0; }
.page-description { margin: .75rem 0 0; color: #52657d; }
.content-section { margin-top: 2rem; }
.content-section h2 { margin: 0 0 .7rem; }
.content-section p { margin: 0; }
.service-grid, .link-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; }
.service-card, .link-card { display: flex; flex-direction: column; gap: .45rem; min-width: 0; padding: 1rem; border: 1px solid #d8e0ea; border-radius: 14px; background: #fbfdff; }
.service-card p, .link-card span { margin: 0; color: #52657d; }
.service-card strong { color: #17643d; }
.button-link, button { display: inline-block; margin-top: 1.25rem; padding: .65rem .9rem; border: 0; border-radius: 9px; background: #176b87; color: #fff; font: inherit; font-weight: 700; text-decoration: none; cursor: pointer; }
.button-link:hover, button:hover { background: #0d4053; }
.contact-form { display: grid; gap: 1rem; margin-top: 1.5rem; }
.form-field { display: grid; gap: .35rem; }
label { font-weight: 700; }
input, textarea { width: 100%; padding: .65rem .75rem; border: 1px solid #aebdca; border-radius: 8px; background: #fff; color: inherit; font: inherit; }
textarea { resize: vertical; }
.contact-form button { justify-self: start; margin-top: 0; }
.status { min-height: 2rem; margin: 0; color: #52657d; }
.status[data-state="success"] { color: #17643d; }
.status[data-state="error"] { color: #a13d2d; }
.lesson-note { margin-top: 1.5rem; padding: 1rem; border: 1px solid #c8e4eb; border-radius: 14px; background: #f2fafb; }
.workflow-list { display: grid; gap: .65rem; padding-left: 1.25rem; }
.site-footer { padding: 2rem 1rem; border-top: 1px solid #d8e0ea; color: #52657d; text-align: center; }
.site-footer p { max-width: 760px; margin: 0 auto; }
@media (max-width: 680px) { .site-header { align-items: flex-start; flex-direction: column; } .service-grid, .link-grid { grid-template-columns: 1fr; } }
@media (max-width: 420px) { .site-header, .page-shell { width: min(100% - 1.25rem, 760px); } .site-header nav { gap: .5rem .75rem; } }
main.js原生 JavaScript 行為:服務陣列、DOM 渲染、FormData 與離線表單狀態。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

const services = [
  { title: "到府清潔", summary: "依空間狀況安排清潔流程。", price: "NT$1,200 起" },
  { title: "定期維護", summary: "建立固定且容易追蹤的保養節奏。", price: "NT$2,400 起" },
  { title: "安心諮詢", summary: "先釐清需求,再決定適合的服務。", price: "免費諮詢" },
];

const contactForm = document.querySelector("#contact-form");
const status = document.querySelector("#status");

function setStatus(message, state = "idle") {
  if (!status) return;
  status.textContent = message;
  status.dataset.state = state;
}

function handleInvalidField() {
  setStatus("請先修正必填欄位或 Email 格式,再送出表單。", "error");
}

function handleContactSubmit(event) {
  event.preventDefault();
  const formData = new FormData(contactForm);
  const payload = Object.fromEntries(formData.entries());
  console.log("LESSON-25 payload", payload);
  setStatus("已完成前端測試送出;目前沒有連線到正式服務。", "success");
}

if (contactForm) {
  contactForm.addEventListener("invalid", handleInvalidField, true);
  contactForm.addEventListener("submit", handleContactSubmit);
}
services.html服務頁:固定三項服務與前往聯絡頁的 CTA。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="LESSON-25 Checkpoint 服務方案。">
  <title>LESSON-25 Checkpoint|服務方案</title>
  <link rel="icon" href="../../../assets/favicon.svg" type="image/svg+xml">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="site-header">
    <a class="brand" href="index.html">安心維護</a>
    <nav aria-label="主要導覽">
      <a href="index.html">首頁</a>
      <a href="services.html" aria-current="page">服務</a>
      <a href="about.html">關於</a>
      <a href="contact.html">聯絡</a>
    </nav>
  </header>
  <main class="page-shell">
    <p class="eyebrow">LESSON-25 · checkpoint</p>
    <h1>服務方案</h1>
    <p class="page-description">三張靜態服務卡片已完成;Solution 會再把它們改成資料陣列。</p>
    <section class="content-section" aria-labelledby="services-title">
      <h2 id="services-title">三項服務</h2>
      <div class="service-grid">
        <article class="service-card"><h3>到府清潔</h3><p>依空間狀況安排清潔流程。</p><strong>NT$1,200 起</strong></article>
        <article class="service-card"><h3>定期維護</h3><p>建立固定且容易追蹤的保養節奏。</p><strong>NT$2,400 起</strong></article>
        <article class="service-card"><h3>安心諮詢</h3><p>先釐清需求,再決定適合的服務。</p><strong>免費諮詢</strong></article>
      </div>
    </section>
    <a class="button-link" href="contact.html">提出諮詢需求</a>
  </main>
  <footer class="site-footer"><p>服務頁:內容已完成,下一步檢查表單欄位。</p></footer>
  <script src="main.js" defer></script>
</body>
</html>
about.html關於頁:單一頁面責任下的背景、工作方式與信任內容。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="LESSON-25 Checkpoint 關於安心維護。">
  <title>LESSON-25 Checkpoint|關於我們</title>
  <link rel="icon" href="../../../assets/favicon.svg" type="image/svg+xml">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="site-header">
    <a class="brand" href="index.html">安心維護</a>
    <nav aria-label="主要導覽">
      <a href="index.html">首頁</a>
      <a href="services.html">服務</a>
      <a href="about.html" aria-current="page">關於</a>
      <a href="contact.html">聯絡</a>
    </nav>
  </header>
  <main class="page-shell">
    <p class="eyebrow">LESSON-25 · checkpoint</p>
    <h1>關於安心維護</h1>
    <p class="page-description">關於頁回答我們是誰、怎麼工作,以及訪客如何確認服務內容。</p>
    <section class="content-section" aria-labelledby="background-title"><h2 id="background-title">我們是誰</h2><p>安心維護從理解現場開始,協助家庭與小型商家整理日常維護。</p></section>
    <section class="content-section" aria-labelledby="process-title"><h2 id="process-title">我們如何工作</h2><p>先聽需求,再確認範圍與預算,最後安排可執行的服務方案。</p></section>
    <section class="content-section" aria-labelledby="trust-title"><h2 id="trust-title">你可以如何確認</h2><p>每一項服務都會先說明內容、結果與下一步。</p></section>
  </main>
  <footer class="site-footer"><p>關於頁:內容責任已完成,不重複服務清單。</p></footer>
  <script src="main.js" defer></script>
</body>
</html>
contact.html聯絡頁:name、email、message 三欄與可觀察送出狀態。

這段要貼在哪裡:先在自己的工作資料夾建立同名相對位置,再複製內容。

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="LESSON-25 Checkpoint 聯絡表單。">
  <title>LESSON-25 Checkpoint|聯絡與諮詢</title>
  <link rel="icon" href="../../../assets/favicon.svg" type="image/svg+xml">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="site-header">
    <a class="brand" href="index.html">安心維護</a>
    <nav aria-label="主要導覽">
      <a href="index.html">首頁</a>
      <a href="services.html">服務</a>
      <a href="about.html">關於</a>
      <a href="contact.html" aria-current="page">聯絡</a>
    </nav>
  </header>
  <main class="page-shell">
    <p class="eyebrow">LESSON-25 · checkpoint</p>
    <h1>聯絡與諮詢</h1>
    <p class="page-description">這一頁只留下最後一個核心缺口:需求欄位有 <code>id</code>,但還沒有 <code>name</code>。</p>
    <form id="contact-form" class="contact-form">
      <div class="form-field"><label for="name">姓名</label><input id="name" name="name" type="text" required></div>
      <div class="form-field"><label for="email">Email</label><input id="email" name="email" type="email" required></div>
      <div class="form-field"><label for="message">需求</label><textarea id="message" rows="5" required></textarea></div>
      <button type="submit">送出諮詢</button>
      <p id="status" class="status" role="status" aria-live="polite" data-state="idle">尚未送出</p>
    </form>
    <aside class="lesson-note"><strong>修正後檢查:</strong>在 DevTools 的 Elements 看 <code>&lt;textarea&gt;</code>,確認它同時有 <code>id="message"</code> 與 <code>name="message"</code>;修正前只會看到 id。</aside>
  </main>
  <footer class="site-footer"><p>Checkpoint:只補上資料欄位的 name。</p></footer>
  <script src="main.js" defer></script>
</body>
</html>

完成驗收

下一步

Checkpoint 完成後,先說出你修正的原因,再前往 Solution 對照每個檔案差異。

前往 Solution,逐檔對照差異