商用網站架設實務

LESSON-25 · starter

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

你會依序完成3個本堂專屬微步驟;每一步只修改一個責任,看到結果後才進下一步。 這頁用 可觀察證據anchor 指出下一個動作。

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

開始前確認

這一頁的結果:四頁可互相到達

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

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

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

照這個順序做

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

TODO-1

讓服務頁只負責回答『提供什麼』以及『下一步怎麼做』。

目的

讓服務頁只負責回答『提供什麼』以及『下一步怎麼做』。

修改檔案

services.html

程式碼位置

services practice area

現在要做:請在 services.html 的「services practice area」位置完成:把練習區替換成三張 service-card 與前往 contact.html 的 CTA

貼上位置:services.html:services practice area 區塊,先保留同一檔案的其他內容。

<section class="content-section"><h2>服務方案</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 href="contact.html">提出諮詢需求</a>

預期結果:服務頁只出現三張卡片,三筆內容都有 title、summary 與 price,且 CTA 可到聯絡頁。

驗收證據:Elements.service-grid 子項目數、三筆文字與 contact.html href。

卡住時先看這裡

常見錯誤:在原有練習區外再新增一組 service-grid,造成重複卡片。

第一個檢查:只在 services.html 搜尋 service-grid,確認整頁只有一個容器。

TODO-2

讓關於頁補足訪客決策需要的背景,不重複服務頁的卡片。

目的

讓關於頁補足訪客決策需要的背景,不重複服務頁的卡片。

修改檔案

about.html

程式碼位置

about practice area

現在要做:請在 about.html 的「about practice area」位置完成:在同一個內容區塊中補上背景、工作方式與信任說明

貼上位置:about.html:about practice area 區塊,先保留同一檔案的其他內容。

<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>

預期結果:about.html 只有一組主要內容區塊,內含三個有清楚標題的 section。

驗收證據:Elements 的主要內容區塊數、三個 h2 與頁面文字。

卡住時先看這裡

常見錯誤:複製 services.html 的服務卡片到關於頁,造成頁面責任重疊。

第一個檢查:先讀 about.html 全部 section,逐一說出每段回答哪個問題。

TODO-3

讓聯絡頁可以收集固定三欄,並在本機回饋送出結果。

目的

讓聯絡頁可以收集固定三欄,並在本機回饋送出結果。

修改檔案

contact.html

程式碼位置

contact practice area

現在要做:請在 contact.html 的「contact practice area」位置完成:在同一個表單中加入 message textarea、submit button 與 status

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

<div class="form-field"><label for="message">需求</label><textarea id="message" name="message" required></textarea></div><button type="submit">送出諮詢</button><p id="status" role="status" aria-live="polite">尚未送出</p>

預期結果:姓名、Email、需求三個欄位都有 label、唯一 id、name 與 required;送出後可看到狀態。

驗收證據:Elementslabel/forid/name、button type、#status 與畫面狀態。

卡住時先看這裡

常見錯誤:只新增輸入框,卻沒有 label、name 或把表單拆成第二個 form。

第一個檢查:只檢查 contact.html 的 form 數量與三個控制項的 id/name/type

階段檔案

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

開啟 starter 的可執行入口

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 Starter 多頁商用網站練習。">
  <title>LESSON-25 Starter|安心維護</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 · starter</p>
    <h1>多頁商用網站</h1>
    <p class="page-description">先把首頁、服務、關於與聯絡四頁連起來,再逐頁補內容。</p>
    <section class="content-section" aria-labelledby="start-title">
      <h2 id="start-title">開始順序</h2>
      <ol class="workflow-list">
        <li>先開啟服務頁,完成服務內容與前往聯絡頁的連結。</li>
        <li>再開啟關於頁,補上背景、工作方式與信任說明。</li>
        <li>最後開啟聯絡頁,補上需求欄位與送出按鈕。</li>
      </ol>
    </section>
  </main>
  <footer class="site-footer"><p>Starter:每次只修改一個頁面責任。</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; }
.placeholder-block { padding: 1rem; border: 1px dashed #d59a5b; border-radius: 14px; background: #fffaf5; }
.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; }
.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">
  <title>LESSON-25 Starter|服務方案</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 · starter · services.html</p>
    <h1>服務方案</h1>
    <p class="page-description">TODO-1:用一個 section 放入三項服務與前往聯絡頁的 CTA。</p>
    <!-- TODO-1:整段替換為三個 service-card 與一個 contact.html 連結。 -->
    <section class="content-section placeholder-block" aria-labelledby="services-title">
      <h2 id="services-title">服務清單練習區</h2>
      <p>尚未加入服務項目。請依 Starter 步驟完成這個 section。</p>
    </section>
  </main>
  <footer class="site-footer"><p>Starter:只替換標記的練習區,不要新增第二個服務 section。</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">
  <title>LESSON-25 Starter|關於我們</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 · starter · about.html</p>
    <h1>關於安心維護</h1>
    <p class="page-description">TODO-2:補上背景、工作方式與信任說明。</p>
    <!-- TODO-2:保留這個 section,將內容補成三個清楚的 h2 區塊。 -->
    <section class="content-section placeholder-block" aria-labelledby="about-title">
      <h2 id="about-title">我們如何協助</h2>
      <p>先理解現場,再提出可執行的服務方案。</p>
    </section>
  </main>
  <footer class="site-footer"><p>Starter:讓關於頁回答訪客的信任問題。</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">
  <title>LESSON-25 Starter|聯絡與諮詢</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 · starter · contact.html</p>
    <h1>聯絡與諮詢</h1>
    <p class="page-description">TODO-3:補上姓名、Email、需求與送出按鈕。</p>
    <!-- TODO-3:在這一個表單中新增需求 textarea 與送出狀態。 -->
    <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>
      <button type="submit">送出諮詢</button>
      <p id="status" class="status" role="status" aria-live="polite" data-state="idle">尚未送出</p>
    </form>
  </main>
  <footer class="site-footer"><p>Starter:先讓表單欄位與 label/id 對得起來。</p></footer>
  <script src="main.js" defer></script>
</body>
</html>

完成驗收

下一步

Starter 完成後,不要直接看 Solution;先前往 Checkpoint,自己找出最後一個核心缺口。

前往 Checkpoint,自己補最後一個核心缺口