跳至主要內容

自訂 Captive Portal:HTML 與 CSS 指南

本權威技術參考指南概述了設計和編寫自訂 Captive Portal 登入頁面所需的開發標準、CSS 架構和網路層級限制。它為前端開發人員和網路架構師提供了實用的策略,以應對 Apple CNA 和 Android webview 環境,確保提供像素級完美、合規且高效能的顧客 WiFi 體驗。

📖 11 分鐘閱讀📝 1,401 字數🔧 2 範例3 練習題📚 8 關鍵定義

收聽此指南

查看播客逐字稿
自訂 Captive Portal:HTML 與 CSS 指南 — Purple 技術簡報 [INTRODUCTION] 歡迎來到 Purple 技術簡報系列。今天我們要深入探討影響每個外來訪客 WiFi 部署的細節 — Captive Portal。具體來說,我們將討論如何為自訂 Captive Portal 登入頁面編寫乾淨、可靠的 HTML 和 CSS。 如果您曾經連接過飯店的 WiFi,卻看到一個損壞的歡迎頁面 — 遺失圖片、未套用樣式的文字、對觸控沒有反應的登入按鈕 — 您就體驗過開發人員在不瞭解其運行環境限制的情況下建立入口網站會發生什麼事。今天,我們要確保這種情況不會發生在您身上。 本簡報專為前端開發人員、創意設計師和網頁開發人員量身打造,不論您是正在從頭開始建立 Captive Portal,還是正在自訂現有的範本。我們將介紹 HTML 結構、至關重要的 CSS 規則、連經驗豐富的開發人員也會出錯的 Apple CNA 微型瀏覽器限制,以及像 Purple 的入口網站建置工具這類的平台如何完全消除大部分的複雜性。 讓我們開始吧。 [TECHNICAL DEEP-DIVE] 首先,讓我們確定 Captive Portal 在網路層級上到底是什麼。當裝置連接到需要驗證的 WiFi 網路時,網路會攔截 HTTP 流量並將使用者重新導向到一個登入頁面。這就是 Captive Portal。使用者會看到一個歡迎頁面,完成一項操作 — 輸入電子郵件、接受條款、透過社群媒體登入 — 然後網路就會授予完整的網際網路存取權限。 要理解的關鍵是這個頁面是在哪裡轉譯的。在 iOS 裝置上,它會在 Apple 的 Captive Network Assistant (即 CNA) 中開啟,這是一個精簡版的 WebKit webview。它不是 Safari。它沒有持續性 Cookie。它無法載入外部資源。它的 JavaScript 支援有限。而且使用者一切換到其他應用程式,它就會關閉。在 macOS 上,CNA 會以固定的 900 x 572 像素進行轉譯。在 Android 上,現代裝置使用 Chrome Custom Tabs,其功能要強大得多。Windows 10 會開啟使用者的預設瀏覽器。Samsung 裝置則使用 Samsung Internet。 這種平台破碎化是生產環境中 Captive Portal 損壞的最大單一原因。開發人員在他們的 Android 手機上進行測試,一切看起來都很棒,然後飯店裡手持 iPhone 的房客卻看到一個只有未套用樣式文字的白畫面。因此,讓我們來談談如何進行防禦性編碼。 Captive Portal HTML 和 CSS 的黃金法則是:將頁面視為沒有網際網路連線。因為在驗證階段,它確實沒有連線。網路是被限制住的。您的頁面嘗試從外部 URL 載入的任何資源 — Google 字型、CDN 託管的樣式表、JavaScript 程式庫、標誌圖片 — 都會無聲地失敗,或者導致載入圖示一直旋轉而無法解決。 首先從 HTML 結構開始。您的文件應該是一個乾淨的 HTML5 頁面。在 head 標記中,您需要一個 viewport meta 標記,其內容設定為 width 等於 device-width,且 initial-scale 等於一。這對於行動裝置的轉譯是不可妥協的。如果沒有它,iOS 會以 980 像素寬度來轉譯頁面並將其縮小,讓所有內容都變得極其微小。 您的 CSS 必須是內聯的 - 可以是 head 元素中 style 區塊內的樣式,或是個別元素上的 inline 樣式屬性。請勿使用透過 link 標記連結的外部樣式表。該樣式表存在於您的伺服器上,在身分驗證期間 Captive Portal 無法存取該伺服器。頁面將會在完全沒有套用樣式的情況下進行轉譯。 字型方面,請使用系統字型系列(system font stack)。例如:font-family — apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif。這會告訴瀏覽器使用任何可用的系統字型。請勿使用 Google Fonts。匯入呼叫將會失敗,而您的備用字型將會變成 Times New Roman,這絕對不是您客戶付費想要獲得的品牌體驗。 對於圖片 - 您的標誌、背景圖形、裝飾元素 - 您有兩個選擇。第一是從同一個 Captive Portal 伺服器提供這些圖片,這意味著它們位於同一個區域網路中,且在身分驗證完成前即可存取。第二是更好的做法,直接在您的 HTML 或 CSS 中將它們編碼為 Base64 資料 URI。這能完全消除外部依賴關係。 現在讓我們來談談頁面版面配置。由於超過百分之九十的 Captive Portal 登入都發生在行動裝置上,您的設計應該以行動裝置優先。這意味著單欄式版面配置,最大寬度約為 480 像素,並在頁面中置中。在 body 元素上使用 flexbox - display flex, flex-direction column, align-items centre, justify-content centre, min-height 100 viewport height。這能將您的內容字卡在任何螢幕尺寸上進行垂直與水平置中。 您的主要行動呼叫按鈕需要便於觸控。Apple 的人機介面指南(Human Interface Guidelines)規定最小點按目標為 44 乘 44 像素。在實務上,對於主要行動呼叫按鈕,您會需要大約 48 像素高、在容器內填滿寬度,且邊角半徑大約為 8 到 12 像素的設計。 對於表單欄位 - 電子郵件輸入框、姓名輸入框 - 請將字型大小設定為至少 16 像素。這點至關重要。iOS Safari 和 CNA 會自動放大任何字型大小低於 16 像素的輸入欄位,這會破壞您精心製作的版面配置。將字型大小設定為 16 像素或以上可以防止這種縮放行為。 法律同意聲明部分需要特別注意。在 GDPR 規範下,如果您正在收集個人資料 - 即使只是電子郵件地址 - 您也需要取得明確且知情的同意。這意味著一個預設不勾選的核取方塊,並附帶一個能清楚說明使用者同意內容的可見標籤。請勿預先勾選該核取方塊。同意核取方塊本身必須清晰可見,無需捲動頁面即可看到。 現在,針對 iOS CNA 的一項關鍵實作細節。當使用者完成驗證時,CNA 會監控 Captive Portal 網域是否已變得可存取。此檢查是由全頁導向觸發,而非 JavaScript AJAX 呼叫。這意味著如果您建立的是單頁應用程式,透過 fetch 或 XMLHttpRequest 提交表單,並在沒有全頁重新導向的情況下更新 DOM,CNA 將永遠無法偵測到驗證已完成。驗證後您必須重新導向至新的 URL - 必須是完整的 HTTP 重新導向,而非 JavaScript DOM 操作。這是 Captive Portal 開發中最常見的錯誤之一。 對於 JavaScript,請保持極簡。CNA 對 JS 的支援有限,且無法存取 localStorage 或 sessionStorage。當 CNA 關閉時,Cookie 就會被銷毀。任何依賴這些瀏覽器 API 的狀態管理都會失敗。使用原生 Vanilla JavaScript 事件監聽器是沒問題的。jQuery 是一個 30 KB 的外部依賴項,會導致載入失敗。 [實作建議與陷阱] 讓我給您實用的實作清單。第一:一律加入 viewport meta 標籤。第二:所有 CSS 採用內聯,不使用外部樣式表。第三:所有圖片皆從 Captive Portal 伺服器提供,或進行 Base64 編碼。第四:使用系統字型集,不使用網路字型。第五:所有輸入欄位的字型大小至少為 16 像素。第六:適合觸控的點選目標,至少 44 乘 44 像素。第七:單欄版面配置,最大寬度 480 像素。第八:驗證時進行全頁重新導向,而非 JavaScript 狀態更新。第九:符合 GDPR 規範的同意核取方塊,預設為未勾選。第十:在實際的 iOS 裝置上使用真實的 Captive Portal 網路進行測試,而非瀏覽器預覽。 我最常在實際環境中看到的陷阱。第一:Google Fonts - 請移除該匯入,否則會失敗。第二:外部 JavaScript 程式庫 - Bootstrap、jQuery,任何 CDN 託管的指令碼都會失敗。第三:在外部樣式表中宣告的 CSS 變數 - 它們必須放在您的內聯樣式區塊中。第四:透過 URL 參照的背景圖片 - 請對其進行 Base64 編碼。第五:在沒有驗證後重新導向的情況下進行 AJAX 表單提交 - CNA 將無法偵測到驗證已完成。 現在,我們來誠實探討自建還是購買。從頭開始構建自訂的 Captive Portal,意味著您還必須負責後端基礎架構 - RADIUS 伺服器、資料庫、SSL 憑證、DNS 設定、與您的存取點進行網路整合,以及持續的安全修補。這是一項重大的工程承諾。 Purple 的門戶建置器為需要像素級精確控制的開發人員提供了拖放式介面以及自訂 HTML 和 CSS 編輯器,同時處理所有的後端基礎架構 - 驗證、資料收集、數據分析、GDPR 合規工具,以及與 200 多家存取點廠商的網路整合。您可以在沒有基礎架構負擔的情況下,獲得創意的自主控制權。 [快速問答] 我可以在 Captive Portal 中使用 CSS Grid 嗎?可以,但請針對 iOS CNA 進行特別測試。Flexbox 在較舊的 WebKit 版本中擁有更廣泛的支援。我可以使用 SVG 標誌嗎?可以,完全支援內嵌 SVG,且其比 Base64 編碼的 PNG 標誌更佳,因為它們在 Retina 顯示器上能完美縮放。macOS CNA 是否支援與 iOS CNA 相同的限制?大致上是的,但有一個區別:macOS CNA 會在固定的 900 x 572 像素視窗中呈現。我可以使用像 Tailwind 這樣的 CSS 框架嗎?只有在您產生經清除、獨立的 CSS 檔案並將其內嵌在 style 區塊中時才可以。那 HTTPS 呢?您的 Captive Portal 必須透過 HTTP 提供服務,初始重定向才能正常運作 - HTTPS 連線無法被 Captive 網路攔截。 [摘要與後續步驟] 總結今天的簡報。自訂的 Captive Portal 是一個受限制的網頁環境,而非標準的瀏覽器環境。Apple CNA 與 Android webviews 對外部資源、Cookie、JavaScript 和工作階段狀態施加了嚴格的限制。解決方案是建立包含內嵌 CSS、系統字型、Base64 編碼圖片的獨立 HTML 網頁,並在驗證時進行全頁重定向。 對於評估其選項的場域營運商與 IT 團隊:如果您的需求是具有自訂 HTML 與 CSS 的完整品牌、專屬入口網站,則可以選擇自行建置與維護完整的技術堆疊(這是一項重大的工程承諾),或是使用像 Purple 這樣在生產級後端基礎架構之上提供自訂 HTML 與 CSS 編輯功能的平台。 後續步驟:檢閱 Purple 的入口網站編輯器文件,根據我們今天涵蓋的行動裝置優先檢核表對您現有的入口網站進行稽核,如果您是從頭開始,請使用我們概述的 HTML 範本結構作為您的基準。感謝您的收聽,我們下期簡報再見。

📚 核心系列的一部分:Captive Portal 終極指南

header_image.png

執行摘要

對於企業級場域 - 從奢華酒店 Hospitality 與零售連鎖店 Retail 到交通樞紐 Transport 以及現代醫療園區 Healthcare - 顧客 WiFi 登入頁面就是數位化大門。然而,超過 90% 的顧客 WiFi 登入發生在行動裝置上,其渲染並非由 Safari 或 Chrome 等標準瀏覽器控制,而是受到高度限制的 Captive Network Assistant (CNA) 網頁檢視 (webview) 所約束 [1]。這些「微型瀏覽器」強制執行嚴格的沙箱限制:它們會阻擋外部 CDN、停用持續性 Cookie、忽略外部網頁字型,並嚴格限制 JavaScript 執行,以降低安全風險並防止工作階段綁架 [2]。

當開發人員使用傳統網頁標準設計登入頁面時,這些限制會導致排版損毀、品牌資產遺失以及登入按鈕失效,直接影響客戶滿意度和數位互動。本指南提供了應對這些挑戰的解決方案,介紹了防禦性編碼實踐 - 例如行內 CSS、Base64 資產編碼、系統字型堆疊以及明確導向導航的驗證交握 - 以確保無縫的跨平台渲染。此外,我們還探討了如何利用像 Purple 的 portal builder 這類託管解決方案,讓開發人員在保持完整 HTML/CSS 創意控制的同時,將 RADIUS 驗證、資料庫擴充、GDPR/PCI 合規性以及多供應商 AP 整合等工作託管出去 [3]。

技術深度剖析

要構建一個具備彈性的自訂 Captive Portal,開發人員必須了解當顧客關聯到開放式 SSID 時所發生的網路層攔截和瀏覽器虛擬化。

Captive Portal 生命週期

當用戶端裝置與 captive SSID 關聯時,會觸發以下程序:

  1. IP 關聯:裝置完成三向交握,並透過 DHCP 請求 IP 位址。
  2. 主動連線探測:作業系統的背景網路管理員會立即向專用的供應商中立 Canary URL(例如 Apple 的 http://captive.apple.com/hotspot-detect.html 或 Google 的 http://connectivitycheck.gstatic.com/generate_204)發送 HTTP GET 請求 [1]。
  3. DNS/HTTP 攔截:本機無線區域網路控制器 (WLC) 或 Access Point (AP) 會攔截此 port 80 HTTP 請求。閘道器不會傳回預期的 HTTP 200 或 204 狀態,而是透過 HTTP 302 重新導向將用戶端的流量重導向至 captive portal 的登陸頁面 URL [2]。
  4. 產生網頁檢視:偵測到重新導向後,作業系統會產生其內建的 Captive Network Assistant (CNA) 微型瀏覽器來顯示重新導向的登入頁面,使用者無需手動開啟完整瀏覽器。
  5. 驗證與狀態轉移:使用者填寫登入表單,將憑證提交回傳送入口伺服器,伺服器會指示閘道器(通常透過 RADIUS Access-Accept 或外部 API 呼叫)授權該 MAC 位址。
  6. CNA 結束握手:CNA 微型瀏覽器對其 Canary URL 執行另一個 HTTP GET。如果收到預期的 200/204 回應,它會將其右上角的按鈕從「取消」變更為「完成」,並將 WiFi 連線建立為主要網路介面。

平台專屬的微型瀏覽器限制

每個作業系統都在不同的 webview 環境中處理此生命週期,從而導致高度破碎的行為。下表詳細列出了這些關鍵限制:

平台 / Webview 顯示方式 永久性 Cookie 外部網頁字型 JavaScript 執行 視窗尺寸 結束握手觸發器
Apple iOS CNA (Websheet) 微型瀏覽器彈出視窗 已封鎖(關閉時銷毀) 已封鎖(離線) 受限(無 localStorage/sessionStorage) 自適應(裝置寬度) 僅限全頁 HTTP 重新導向 [1]
Apple macOS CNA (Captive Network Assistant) 微型瀏覽器彈出視窗 已封鎖 已封鎖 受限(無 alert/confirm 對話框) 固定 (900px x 572px) 僅限全頁 HTTP 重新導向
Android (Google) (CaptivePortalLogin) 推播通知 -> Chrome Custom Tab 允許(與 Chrome 共用) 允許(若在圍牆花園白名單中) 完整 自適應 自動 (Captive Portal API / 204 檢查) [2]
Samsung Android (Samsung Internet) 推播通知 -> 微型瀏覽器 允許 允許 完整 自適應 自動
Windows 10/11 (預設瀏覽器) 自動啟動預設瀏覽器 允許(完整瀏覽器環境) 允許 完整 自適應 手動 / 自動

cna_constraints_comparison.png

繞過 Apple CNA「完成」按鈕陷阱的編碼方法

自訂傳送入口開發中最常見的失敗模式之一是 iOS 裝置上的**「完成」按鈕陷阱**。當使用者進行驗證時,iOS Websheet webview 必須偵測到網路不再處於受限狀態。它透過監控其背景 Canary 請求的成功與否來做到這一點。

至關重要的是,iOS CNA 僅會在全頁面 HTTP 導向(位置重導向)時觸發此檢查。如果開發人員建置了一個現代單頁應用程式(SPA),並透過非同步 AJAX 呼叫(例如 fetch()Axios)提交表單資料,並在不變更 URL 的情況下動態更新 DOM,CNA 將 永遠不會 重新執行其連線檢查。使用者將在閘道層級通過驗證,但右上方角的 CNA 按鈕仍將保持為「取消」。如果感到挫折的使用者點擊「取消」,iOS 裝置將立即與 SSID 中斷關聯,終止 WiFi 工作階段 [1]。

為防止這種情況,驗證成功處理常式 必須 執行全頁面重導向至實體落地網頁(例如 window.location.href = '/success'),或透過標準 HTTP POST 動作以原生方式提交登入表單。

實作指南

為確保在所有平台上的呈現一致,開發人員必須從現代、資源密集的網頁設計,轉變為高度自包含、防禦性的編碼風格。

黃金法則:針對零網際網路連線進行設計

在 Captive Portal 狀態下,用戶端裝置無法存取更廣泛的網際網路。它只能解析和存取無線控制器 Walled Garden 中明確列入白名單的 IP 位址和網域(例如 Captive Portal 伺服器本身的 IP)。因此,HTML 中引用的任何外部資源都將無法載入,從而導致版面配置損壞。

為了進行防禦性設計,請實作以下 行動優先 Captive Portal 設計檢查清單

mobile_first_checklist.png

1. 檢視區配置

為了防止行動裝置將檢視區縮小至桌上型電腦寬度(通常為 980px),HTML <head> 必須包含一個響應式檢視區 meta 標籤。如果沒有此標籤,文字和輸入欄位在行動裝置上會顯得微乎其微:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

2. 內嵌 CSS 並移除外部相依性

切勿連結到外部 CSS 檔案或 CDN(例如 Bootstrap、Tailwind 或 Google Fonts)。所有 CSS 都必須嵌入 HTML <head> 內的 <style> 區塊中。

對於字型,請使用引用預先安裝的本機作業系統字型的系統字型堆疊。這完全避免了 HTTP 請求,並確保使用裝置的原生、高品質無襯線字型立即載入網頁:

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    background-color: #F5F1ED; /* Pearl White 品牌色彩 */
    color: #011638; /* Dark Blue 品牌色彩 */
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
}

3. Base64 資產編碼

除非代管網域已被列入閘道器的圍牆花園(Walled Garden)白名單,否則不應透過外部 HTTP URL 參照圖片、標誌(Logo)和圖示。最可靠的方法是將小型視覺資產(例如 PNG 或 SVG 標誌)直接編碼為 Base64 資料 URI,嵌入至 HTML 或 CSS 中:

<!-- 行內 Base64 標誌範例 -->
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgZmlsbD0iIzc0NThGRCIvPjwvc3ZnPg==" alt="Purple Logo" class="brand-logo">

4. 輸入欄位與點擊目標最佳化

為符合行動裝置可用性標準(例如 Apple 的 Human Interface Guidelines),所有互動式元素都必須針對觸控輸入進行最佳化:

  • 輸入欄位字型大小:輸入文字字型大小必須至少為 16px。如果字型大小較小(例如 14px),iOS Safari 和 CNA 在聚焦該輸入欄位時會自動放大,導致頁面排版變形 [1]。
  • 點擊目標:按鈕、核取方塊和連結的最小觸控目標區域必須為 44px x 44px,以防止誤觸或造成使用者困擾。

5. 法律同意與 GDPR 合規性

擷取訪客資料(例如電子郵件、電話號碼或社群設定檔)時,HTML 結構必須支援明確的法律同意。在 GDPR 規範下,預先勾選的核取方塊或隱含的同意聲明(例如「登入即代表您同意我們的條款」)皆不合規 [3]。

  • 為服務條款和隱私權政策提供一個預設為未勾選的核取方塊。
  • 將詳細的法律條文封裝在一個可捲動、限制高度的容器中(max-height: 120px; overflow-y: auto;),以確保主要的登入表單保持在首頁上半部(Above the Fold)。
<div class="consent-container">
    <input type="checkbox" id="terms_consent" name="terms_consent" required>
    <label for="terms_consent">
        我明確同意 <a href="#" onclick="showTerms()">服務條款</a>,並同意依據 <a href="#" onclick="showPrivacy()">隱私權政策</a> 處理我的資料。
    </label>
</div>

自訂 Captive Portal HTML/CSS 範本

以下是一個完整、可用於生產環境的單一檔案 HTML/CSS 範本,融合了所有行動裝置優先、符合 CNA 規範的最佳實踐。它具備響應式排版、系統字型堆疊、Base64 專用資產預留位置、觸控友好的輸入欄位,以及符合 GDPR 規範的明確同意結構:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Guest WiFi Login</title>
    <style>
        /* CSS Reset & Variable Declarations */
        :root {
            --color-primary: #7458FD; /* Purple Brand Colour */
            --color-dark: #011638; /* Dark Blue */
            --color-light: #F5F1ED; /* Pearl White */
            --color-white: #FFFFFF;
            --color-border: #D1D5DB;
            --radius-container: 12px;
            --radius-element: 6px;
        }
        
        * {
            box-sizing: border-box;
            -webkit-tap-highlight-color: transparent;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            background-color: var(--color-light);
            color: var(--color-dark);
            margin: 0;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
        }
        
        /* 響應式卡片版面配置 */
        .portal-card {
            background-color: var(--color-white);
            width: 100%;
            max-width: 420px;
            padding: 32px 24px;
            border-radius: var(--radius-container);
            box-shadow: 0 4px 20px rgba(1, 22, 56, 0.05);
            text-align: center;
        }
        
        .logo-container {
            margin-bottom: 24px;
        }
        
        /* 內嵌 Base64 Logo 預留位置 */
        .logo {
            width: 120px;
            height: auto;
        }
        
        h1 {
            font-size: 24px;
            font-weight: 700;
            margin: 0 0 8px 0;
            color: var(--color-dark);
        }
        
        p {
            font-size: 14px;
            color: #4B5563;
            margin: 0 0 24px 0;
            line-height: 1.5;
        }
        
        /* 表單樣式 */
        .form-group {
            text-align: left;
            margin-bottom: 16px;
        }
        
        label {
            display: block;
            font-size: 14px;
            font-weight: 600;
            margin-bottom: 6px;
            color: var(--color-dark);
        }
        
        /* 重要:字型大小必須至少為 16px,以防止 iOS 自動縮放 */
        input[type="email"],
        input[type="text"] {
            width: 100%;
            height: 48px;
            padding: 12px 16px;
            font-size: 16px;
            border: 1px solid var(--color-border);
            border-radius: var(--radius-element);
            background-color: var(--color-white);
            color: var(--color-dark);
            outline: none;
            transition: border-color 0.2s;
        }
        
        input[type="email"]:focus,
        input[type="text"]:focus {
            border-color: var(--color-primary);
            box-shadow: 0 0 0 3px rgba(116, 88, 253, 0.15);
        }
        
        /* 同意聲明樣式 */
        .consent-group {
            display: flex;
            align-items: flex-start;
            text-align: left;
            margin: 20px 0;
        }
        
        /* 適合觸控操作的目標尺寸 */
        input[type="checkbox"] {
            width: 20px;
            height: 20px;
            margin: 2px 12px 0 0;
            flex-shrink: 0;
            cursor: pointer;
            accent-color: var(--color-primary);
        }
        
        .consent-label {
            font-size: 13px;
            line-height: 1.4;
            color: #4B5563;
            user-select: none;
        }
        
        .consent-label a {
            color: var(--color-primary);
            text-decoration: none;
            font-weight: 600;
        }
        
        .consent-label a:hover {
            text-decoration: underline;
        }
        
        /* Touch-Friendly CTA Button (Minimum 44px height) */
        .btn-submit {
            width: 100%;
            height: 48px;
            background-color: var(--color-primary);
            color: var(--color-white);
            border: none;
            border-radius: var(--radius-element);
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: background-color 0.2s;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .btn-submit:hover {
            background-color: #5B3EE3;
        }
        
        .btn-submit:active {
            background-color: #4A2FD4;
        }
        
        /* Legal Terms Scrollbox */
        .terms-scrollbox {
            display: none; /* Toggled via JS */
            text-align: left;
            font-size: 11px;
            color: #6B7280;
            background-color: var(--color-light);
            padding: 12px;
            border-radius: var(--radius-element);
            max-height: 100px;
            overflow-y: auto;
            margin-bottom: 16px;
            border: 1px solid var(--color-border);
        }
        
        .footer {
            margin-top: 24px;
            font-size: 11px;
            color: #9CA3AF;
        }
    </style>
</head>
<body>

    <div class="portal-card">
        <div class="logo-container">
            <!-- Replace src with your actual Base64-encoded SVG or PNG -->
            <svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 30">
                <rect width="100" height="30" rx="6" fill="#7458FD"/>
                <text x="50" y="20" font-family="sans-serif" font-size="12" fill="#FFFFFF" font-weight="bold" text-anchor="middle">您的品牌</text>
            </svg>
        </div>
        
        <h1>歡迎使用訪客 WiFi</h1>
        <p>請在下方輸入您的詳細資訊以取得安全的快速度網路連線。</p>
        
        <!-- Form must submit natively (full page navigation) to clear Apple CNA -->
        <form action="/login/submit" method="POST" onsubmit="return validateForm()">
            <div class="form-group">
                <label for="guest_name">姓名</label>
                <input type="text" id="guest_name" name="name" placeholder="王小明" required autocomplete="name">
            </div>
            
            <div class="form-group">
                <label for="guest_email">電子郵件地址</label>
                <input type="email" id="guest_email" name="email" placeholder="john@example.com" required autocomplete="email">
            </div>
            
            <div class="consent-group">
                <input type="checkbox" id="terms_consent" name="terms_accepted" required>
                <label for="terms_consent" class="consent-label">
                    我接受 <a href="#" onclick="toggleTerms(event)">服務條款</a> 並同意依照 GDPR 規範進行數據處理。
                </label>
            </div>
            
            <div id="terms_box" class="terms-scrollbox">
                <strong>WiFi 服務條款:</strong><br>
                1. 本服務按「現況」提供,不提供任何保證。<br>
                2. 使用者不得從事非法或佔用高頻寬的活動。<br>
                3. 個人數據的收集僅用於身分驗證與行銷訂閱,並符合我們的隱私權政策。<n>
            </div>
            
            <button type="submit" class="btn-submit">連接至 WiFi</button>
        </form>
        
        <div class="footer">
            Powered by Purple | 安全訪客 WiFi
        </div>
    </div>

    <script>
        function toggleTerms(e) {
            e.preventDefault();
            var box = document.getElementById('terms_box');
            box.style.display = (box.style.display === 'block') ? 'none' : 'block';
        }
        
        function validateForm() {
            var consent = document.getElementById('terms_consent');
            if (!consent.checked) {
                alert('您必須接受服務條款才能進行連接。');
                return false;
            }
            return true;
        }
    </script>
</body>
</html>

疑難排解與風險規避

在部署自訂程式碼的 HTML/CSS Captive Portal 時,IT 運維團隊經常會遇到幾個嚴重的營運風險:

1. SSL/TLS 憑證警告無限循環

由於 Captive Portal 是透過攔截流量來運作,這與現代 HTTPS 網路安全機制存在根本上的衝突。當使用者嘗試造訪 HTTPS 網站(例如 https://www.google.com),而閘道器企圖將該流量重新導向至 HTTP Captive Portal 時,瀏覽器會偵測到 SSL 憑證不符合,並顯示嚴重的「您的連線不是安全連線」安全警告。

  • 規避措施:切勿嘗試直接攔截 HTTPS 流量。應完全依賴作業系統內建的 CNA 輔助程式(該程式會發送未加密的 HTTP 請求以觸發重新導向)。確保您的 Captive Portal 網域擁有有效且受公開信任的 SSL 憑證(例如 Let's Encrypt 或 DigiCert),且僅在初始 HTTP 重新導向成功將使用者路由至您的入口網站網域後,才透過 HTTPS 提供服務 [2]。

2. DNS 解析失敗(圍牆花園陷阱)

如果您的自訂 HTML 頁面引用了外部資源(例如 Facebook、Google 等社群登入 OAuth 端點,或付款閘道),除非將這些網域明確列入無線控制器的 Walled Garden 白名單中,否則針對這些網域的 DNS 請求將會失敗。如果白名單中遺漏了某個網域,登入流程將會停滯並顯示空白畫面。

  • 因應對策:維護一份嚴格且精簡的 Walled Garden 清單。如果使用社群登入,請將身分驗證提供者建議的特定萬用字元網域列入白名單(例如 *.google.com*.gstatic.com)。

3. 工作階段逾時與 MAC 偽造漏洞

標準的 Captive Portal 是根據裝置的 MAC 位址進行驗證。然而,現代行動作業系統(iOS 14+ 和 Android 10+)預設使用隨機 MAC 位址(私用 WiFi 位址),並會定期輪替。這可能會導致訪客重複收到重新驗證的提示,進而破壞使用者體驗 [1]。

  • 因應對策:在 RADIUS 伺服器上實施合理的工作階段逾時設定(例如 24 小時)以防止過期工作階段,並採用 Passpoint (Hotspot 2.0)WPA3-Enterprise 等現代驗證標準,實現完全繞過基於 MAC 的 Captive Portal 的無縫、安全註冊流程。

Purple 產品關聯性:自行建置 vs. 直接購買

雖然編寫單個 HTML 頁面非常簡單,但託管、保護和擴充自訂的 Captive Portal 基礎架構會面臨巨大的技術和合規性障礙。下表比較了自行託管自訂入口網站與使用 Purple 的託管企業平台在工程與營運上的現實差異:

功能 / 營運需求 自行託管的自訂入口網站 Purple 企業級 WiFi 平台
HTML/CSS 自訂 完全手動編寫程式碼,並將檔案上傳到個別 AP 或本機網頁伺服器。 像素級精準的開發人員編輯器,允許自訂 HTML/CSS 嵌入,並結合拖放式視覺化建置工具。
RADIUS 基礎架構 必須部署、設定並維護高可用性的 FreeRADIUS 或雲端 RADIUS 伺服器 [4]。 內建、全球分佈、雲端原生 RADIUS,提供雙活(Active-Active)備援和 99.99% 的可用性 SLA。
多廠商 AP 支援 每個硬體廠商(Cisco、Aruba、Meraki、Ruckus)都需要自訂整合指令碼 [5]。 與 200 多種硬體型號進行原生、即開即用的整合;在混合硬體的資產中實現統一的入口網站部署。
資料隱私與合規性 場所需承擔 100% 的 GDPR、CCPA 和 PCI-DSS 合規性法律責任,包括安全的資料庫加密和資料刪除工作流程。 設計上完全合規。內建同意聲明管理、自動化資料主體刪除請求,以及安全的 ISO 27001 認證託管。
Identity Provider Integrations Manual OAuth2 integrations with Google, Facebook, Apple, and local SMS gateways. One-click integrations with major social platforms, SMS gateways, and Azure AD / Okta for corporate guests.

Purple 的平台解決了「自行建置還是外購」的兩難。它為開發人員提供了自訂 HTML/CSS 工作區的完整創意自由,同時消除了大規模支援安全 RADIUS 驗證所需、複雜且高風險的後端基礎架構工程。

投資報酬率與商業影響力

投資專業設計、具備自適應特性的自訂 Captive Portal,可在 IT 營運、行銷和法規遵循方面帶來可量化的回收。

1. 降低營運成本 (IT 客服中心支援工單)

在體育場或多據點零售連鎖店等大規模部署中,故障的 Captive Portal 是導致 IT 客服中心升級工單的主要原因。當訪客遇到「白畫面」或無法回應的登入按鈕時,他們會讓現場工作人員應接不暇,或提交支援工單。

$$\text{年度支援省下成本} = (\text{年度訪客總造訪次數} \times \text{Portal 失敗率} \times \text{客服中心聯繫率}) \times \text{每張支援工單成本}$$

  • 情境:一個每年有 1,000,000 名訪客的會議中心。一個程式碼編寫不良的 Portal 在舊版 iOS 裝置上的失敗率為 5%,導致 10% 的客服中心聯繫率。以業界標準每張支援工單 15 美元計算,營運成本為: $$(1,000,000 \times 0.05 \times 0.10) \times $15 = $75,000 \text{ 每年可避免的支援開銷}$$
  • 結果:轉換為針對 CNA 最佳化、行動優先的範本,可將 Portal 失敗率降低至 <0.1%,幾乎消除了這項營運損耗。

2. 行銷數據擷取與訂閱同意最佳化

對於零售和旅宿場所,訪客 WiFi Portal 是擷取乾淨的第一方顧客數據的主要機制。使用者介面如果設計不良,字型過小或表單版面配置笨重,會導致高跳出率 - 使用者會完全放棄登入程序,從而失去行銷機會。

  • 案例研究 (零售):某家全國零售連鎖店採用了利用 Purple 平台建置的行動優先最佳化 Captive Portal。透過將多步驟登入表單替換為單一欄位電子郵件輸入 (字型大小:16px) 以及最佳化的 48px 點擊目標按鈕,他們在第一季內實現了完成註冊量增加 42% 以及行銷電子報訂閱同意率增加 28% 的成果 [6]。

3. 法律與法規風險規避

在 GDPR 與 CCPA 的規範下,不合規的數據收集會帶來嚴厲的財務處罰(在 GDPR 下最高可達全球年營業額的 4%)。依賴預先勾選的核取方塊,或未能於登入頁面提供清晰且易於存取的隱私權政策,會使企業面臨巨大的法律責任。

  • 降低風險的投資報酬率:實施明確且預設未勾選的同意核取方塊,並在最佳化的捲動文字方塊中託管條款,可確保 100% 符合法規,從而降低數百萬美元法規罰款的風險,並保護品牌聲譽。

重點摘要

  • CNA 沙箱具限制性:Apple 的 iOS Websheet 與 macOS CNA 是高度沙箱化的環境,會封鎖外部資源、Cookie 與網頁字型。所有樣式與資源必須自包含(行內 CSS、Base64 圖片、系統字型) [1]。
  • AJAX 會破壞 iOS 結束握手:若要成功將 iOS 裝置從「受限連線」轉換為「已連線」(將右上角按鈕從「取消」變更為「完成」),您必須觸發全頁面 HTTP 重新導向。非同步 DOM 更新會使裝置陷入受限連線的無限循環中。
  • 行動優先為強制要求:超過 90% 的登入發生在行動裝置上。請設計單欄版面配置(最大寬度:480px)、使用適合觸控的點選目標(至少 44px x 44px),並對所有文字輸入欄位強制執行至少 16px 的字型大小,以防止 iOS 瀏覽器自動縮放。
  • 圍牆花園(Walled Gardens)控制 DNS:登入過程中參照的任何外部網域(例如社群登入 API)都必須在無線控制器的圍牆花園中明確列入白名單,否則頁面將無法載入。
  • Purple 消除後端複雜性:使用 Purple 的入口網站建置工具,開發人員可透過自訂編輯器獲得完整的 HTML/CSS 控制權,同時卸下 RADIUS、多供應商 AP 整合以及符合 GDPR 的資料庫管理等在安全性、擴充性與合規性方面的巨大負擔 [3]。

參考文獻


收聽技術簡報

收聽資深方案架構師討論自訂 Captive Portal 的技術限制與部署策略:

關鍵定義

Captive Portal

在向新連接的 WiFi 網路使用者授予更廣泛的網路資源存取權限之前,向其顯示的網頁,通常用於驗證、付款或顯示服務條款。

IT 團隊在閘道層級部署 Captive Portal,以控制訪客存取、收集使用者資料並執行法律合規性。

Captive Network Assistant (CNA)

由作業系統(例如 Apple iOS 和 macOS)在偵測到 Captive 網路重新導向時自動產生的極度受限、沙盒化的微型瀏覽器,其目的僅在於促進入口網站驗證。

CNA 網頁檢視執行嚴格的限制,包括封鎖外部 CDN、持續性 Cookie 和本機儲存,這經常會破壞標準的網頁設計。

Walled Garden

一個受限制的 IP 位址、子網路或網域名稱清單,未經驗證的訪客使用者在完成 Captive Portal 登入程序之前,被允許透過閘道存取這些內容。

開發人員必須確保任何外部資源(例如社群登入 API 或付款閘道)都已列入 Walled Garden 的白名單,以防止登入流程停滯。

Base64 Encoding

一種將二進位資料(如影像)表示為 ASCII 字串的二進位轉文字編碼配置,允許將資產直接內嵌在 HTML 或 CSS 文件中。

利用 Base64 Encoding 處理標誌和圖示可以消除外部 HTTP 請求,確保資產在離線 CNA 環境中完美呈現。

RADIUS (Remote Authentication Dial-In User Service)

一種網路協定,為連接和使用網路服務的使用者提供集中式的驗證、授權和計帳(AAA)管理。

一旦滿足驗證條件,Captive Portal 伺服器就會與 RADIUS 伺服器通訊,在網路閘道端對訪客的 MAC 位址進行授權。

System Font Stack

一種 CSS font-family 宣告,優先使用預先安裝的作業系統字型(例如 iOS 上的 San Francisco、Windows 上的 Segoe UI 以及 Android 上的 Roboto),而非外部網路字型。

實作 System Font Stack 可確保立即轉譯排版,而不會觸發針對 Google Fonts 等服務被封鎖的外部 HTTP 請求。

Canary URL

由作業系統廠商維護的專用、未加密 HTTP URL(例如 captive.apple.com),用於測試裝置是否擁有不受限制的網際網路連線能力。

作業系統背景網路管理器會檢查此 URL,以偵測是否存在 Captive Portal 並觸發 CNA 網頁檢視彈出視窗。

Passpoint (Hotspot 2.0)

由 Wi-Fi Alliance 開發的產業標準,使行動裝置能夠自動發現 Wi-Fi 熱點並進行安全驗證,跳過手動的 Captive Portal 登入流程。

企業將 Passpoint 與 Purple 等平台結合使用,讓訪客從繁瑣的歡迎頁面過渡到無縫、類似行動網路的安全漫遊體驗。

範例

一家擁有 250 間客房的奢華連鎖飯店 [飯店餐旅業](/industries/hospitality) 希望實作一個完全符合其頂級品牌形象的自訂顧客 WiFi 登入頁面。他們的廣告代理商設計了一個啟動頁面,使用自訂的品牌字型(託管於 Adobe Fonts)、多個高解析度背景圖片(託管於公開的 AWS S3 儲存桶)以及多步驟的 JavaScript 動效精靈。部署後,iOS 顧客連接到 SSID,但彈出的 Portal 頁面卻是一片空白,且使用者無法進行驗證。

要解決空白畫面和品牌破圖的問題,我們必須重構 Portal 的前端架構,以符合 Apple CNA 沙盒限制:

  1. 字型修復:由於 Adobe Fonts 需要外部 HTTP 請求,而該請求會被 CNA 阻擋,我們將自訂字型呼叫替換為原生、優質的系統字型集(font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;)。這可確保無需外部網路呼叫即可立即渲染。
  2. 資產最佳化:AWS S3 上的背景圖片被阻擋,因為 S3 不在閘道器的圍牆花園(Walled Garden)中。我們壓縮主要品牌標誌,將其轉換為輕量級的 SVG,並將其作為 Base64 Data URI 直接編碼在 HTML 中。對於背景,我們使用飯店品牌色彩的簡潔、響應式 CSS 漸層取代沉重的圖片,從而大幅減輕頁面大小。
  3. JavaScript 簡化:多步驟動效精靈依賴外部 jQuery 和 GSAP 程式庫。我們去除這些外部依賴關係,並將表單重構為單頁、單欄的 HTML 結構。表單驗證則使用輕量級的原生 JavaScript 重寫。
  4. 驗證交握:表單提交從基於 AJAX 的提交修改為原生的 HTML <form action="/submit" method="POST">,以觸發全頁面重導向,使 iOS Websheet 能夠執行其 Canary 測試並顯示「完成」按鈕。
考官評語: 此情境代表了高端創意設計與 Captive Webview 嚴格安全限制之間的典型衝突。廣告代理商通常會將 Captive Portal 視為標準的桌上型電腦網站。然而,由於裝置處於預先驗證狀態,網路會阻擋所有外部流量。透過內嵌 CSS、使用系統字型、Base64 編碼資產以及利用原生表單提交,我們在保留頂級品牌美學的同時,在 iOS 和 Android 裝置上實現了 100% 的運作可靠性。

一家擁有 450 家門市的連鎖零售商 [零售業](/industries/retail) 希望透過 WiFi 啟動頁面收集顧客電子郵件,以充實其 CRM。他們要求顧客選擇訂閱行銷電子報。初始設計包含一個預先勾選的「我同意接收行銷電子郵件」核取方塊。此外,該 Portal 託管在總部的單一本地伺服器上。在尖峰時段(週六下午),全國各地的顧客都會遇到嚴重的延遲,許多人無法載入登入頁面,導致數據收集率大幅下降。

我們必須同時解決合規性違規和基礎架構瓶頸問題:

  1. 合規性補救措施:在 GDPR 和 CCPA 的規定下,預先勾選的同意方塊是非法的。我們修改 HTML,將行銷同意核取方塊預設設為未勾選(<input type="checkbox" id="marketing_consent">)。我們還為服務條款添加了一個獨立的、強制性的核取方塊,以將法律協議與行銷訂閱脫鉤。
  2. 基礎架構擴充:將全國性的 Captive Portal 託管在單一集中式伺服器上會建立單一故障點,並造成巨大的延遲瓶頸。我們將入口網站前端遷移到具有邊緣快取、高可用性且全球分佈的內容傳遞網路(CDN)。
  3. RADIUS 整合:我們設定本地商店的存取點,指向具有主動 - 主動備援的雲端原生 RADIUS 叢集,確保即使在週六的流量尖峰期間,驗證請求也能在邊緣端以低於 50 毫秒的延遲進行本地處理。
  4. Purple 遷移:為了消除這整個工程開銷,零售商遷移到了 Purple。Purple 內建的 GDPR 同意工具會自動管理符合合規要求的訂閱,其全球分佈的雲端基礎架構能以 99.99% 的上線時間處理每日數百萬次的驗證,徹底解決了擴充瓶頸。
考官評語: 預先勾選的同意核取方塊代表了嚴重的合規風險,可能會導致巨額的監管罰款。將行銷同意與服務條款脫鉤是技術和法律上的最佳實踐。在基礎架構方面,集中式託管 Captive Portal 是一種反模式。全國性的零售業務版圖需要去中心化、邊緣快取的前端,並結合雲端原生的 RADIUS 後端。遷移到像 Purple 這樣的託管平台可以消除這種架構的複雜性,讓零售商能夠專注於行銷活動,而不是資料庫擴充。

練習題

Q1. 一家大型國際機場 [Transport](/industries/transport) 的 IT 團隊部署了自訂編碼的 Captive Portal。他們發現,雖然 Android 使用者可以無縫連線,但很大一部分的 iOS 使用者遇到了成功驗證卻無法瀏覽網頁的問題。仔細觀察後,這些 iOS 裝置顯示已連線到 SSID,但強制登入彈出視窗右上角的按鈕仍顯示「取消」而非「完成」。此問題的根本原因為何?開發人員應如何修正?

提示:分析 Apple CNA 小幫手如何偵測網路已從受限狀態轉變為已驗證狀態,以及需要何種瀏覽器動作來觸發此檢查。

查看標準答案

根本原因是該入口網站的成功頁面是透過 JavaScript(AJAX/SPA 路由)動態更新 UI,而不是執行整頁的 HTTP 導航。Apple iOS Captive Network Assistant (CNA) 迷你瀏覽器只有在發生整頁 URL 重新導向或導航時,才會重新執行其背景連線檢查(對 captive.apple.com 的 canary 請求)。如果開發人員透過 AJAX 送出登入表單並僅在 DOM 中顯示「成功」訊息,CNA 將無法得知網路已被解除鎖定。因此,右上角的按鈕仍保持為「取消」。如果使用者按「取消」結束,作業系統會假設登入失敗並中斷與 WiFi 網路的連線。

解決方案:開發人員必須修改驗證成功處理常式以強制進行整頁重新導向。這可以透過使用標準 HTML <form action="/submit" method="POST"> 原生送出登入表單來實現,或者在 API 傳回成功的驗證回應後,在 JavaScript 中執行 window.location.href = '/success_landing_page'。這會觸發所需的整頁載入,強制 CNA 小幫手重新評估網路狀態,確認現在可以連線到 canary URL,並將右上角的按鈕變更為「完成」。

Q2. 一個體育場營運團隊 [Events] 想要推出一個可收集行銷訂閱的訪客 WiFi 網路。合規官員堅持該入口網站必須 100% 符合 GDPR 規範。開發團隊展示了一個原型,其中登入表單有一個預先勾選的方塊,上面寫著「我同意服務條款並同意接收行銷電子報」。為什麼此設計不合規?在保持高轉換率的同時,應如何重構 HTML/CSS 和表單結構以滿足 GDPR 要求?

提示:請考慮 GDPR 對於明確同意、行銷訂閱與服務條款之解耦,以及法律條文在行動裝置螢幕上的物理可見性的嚴格要求。

查看標準答案

這項提議的設計在兩個主要方面違反了 GDPR:首先,預先勾選的核取方塊不構成有效的同意,同意必須是自由給予、具體、知情且明確的。其次,將行銷同意與服務條款協議捆綁在一起是不合規的;不能強制使用者接受行銷電子郵件,以此作為使用 WiFi 服務的條件。

重構策略

  1. 拆分同意書:將核取方塊拆分為兩個獨立的核取方塊。核取方塊 A 為必填,涵蓋服務條款與隱私權政策。核取方塊 B 為選填,涵蓋訂閱行銷電子報。
  2. 預設為取消勾選:確保 HTML 中的兩個核取方塊預設皆為未勾選狀態(省略 checked 屬性)。
  3. CSS 可見性:由於超過 90% 的使用者使用行動裝置,請將核取方塊直接置於「連線」按鈕上方,使其在不滾動的情況下處於「首屏」可見位置。使用系統字型集,並將標籤字型大小設定為 14px,行高設定為 1.4 以利閱讀。
  4. 條款滾動框:為避免法律條文將表單元素擠出螢幕,請將詳細的服務條款放置在具有固定高度的可滾動容器中(max-height: 100px; overflow-y: auto; background-color: #F5F1ED; border: 1px solid #D1D5DB; border-radius: 6px;),該容器可透過文字連結切換展開或關閉。這樣可以在維持乾淨、高轉換率版面的同時,確保絕對符合法律合規性。

Q3. 某家零售連鎖品牌 [Retail](/industries/retail) 正在 100 家分店部署自訂程式碼的歡迎頁面。設計師使用了 Google Fonts (Montserrat),並在 HTML 的 head 中連結了 CDN 代管的 Bootstrap 樣式表。在企業網路測試期間,頁面轉譯效果非常完美。然而,當部署到配置了 Captive Portal 的測試分店 AP 時,頁面轉譯卻出現了未套用樣式的 Times New Roman 文字、排版錯亂以及圖示缺失。為什麼會發生這種情況?該如何重構這些資產?

提示:分析使用者通過驗證前的網路連線狀態,並確定瀏覽器如何處理對圍牆花園(Walled Garden)以外網域的外部 HTTP 請求。

查看標準答案

發生此失敗的原因是,當載入歡迎頁面時,裝置處於未經驗證的受限狀態。在此狀態下,無線閘道器會封鎖所有連外網路流量,僅允許對閘道器圍牆花園(Walled Garden)中明確列入白名單的網域進行請求。由於 Bootstrap (cdn.jsdelivr.net) 和 Google Fonts (fonts.googleapis.com) 的 CDN 網域未列入白名單,瀏覽器擷取樣式表和字型檔案的請求會默默失敗。因此,瀏覽器會退回到其預設的轉譯引擎,導致 HTML 未套用樣式(呈現 Times New Roman 文字)且版面錯亂。

重構策略

  1. 內聯 CSS:移除外部 Bootstrap 樣式表連結。將所需的 CSS grid/flexbox 規則直接複製到 HTML <head> 中的 <style> 區塊中。這可確保所有版面配置指令都包含在初始的單一頁面酬載(Payload)中。
  2. 實作系統字型集:移除 Google Fonts 的 @import<link> 呼叫。在 CSS 中將其替換為原生系統字型集(font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;)。這會強制裝置使用作業系統中已預先安裝的高品質字型,從而完全消除對外部網路的依賴。
  3. Base64 編碼圖示/標誌:如果版面依賴外部圖片或圖示庫(如 FontAwesome),請將這些圖示轉換為 SVG 格式,並將其內嵌在 HTML 中,或作為 CSS 中的 Base64 Data URI。這能保證頁面 100% 自帶完整內容,即使在完全沒有網路連線的情況下也能完美轉譯。