📅 2026年4月 — 站務變更(至 4/11)

對應資料夾 2604/;與 git_hub 總覽 併讀。

時間範圍

2026/04/01~2026/04/11(含當日前後相關 commit)。

重點條目(依主題)


2026/04/11 — Claude AI 輔助三項重構

以下三項均由 Claude Sonnet 在單次對話完成,對話記憶涵蓋整個 repo 脈絡。

1. 條件式側邊欄(Conditional Sidebar)

需求:進入 /kid/ 時只顯示 Kid 選單,大人的 SSD/TOEIC/Investment 全部隱藏;不拆 repo、不分站。

解法_includes/sidebar.html 頭尾加 Liquid 條件判斷:

{% if page.url contains '/kid/' %}
  <!-- Kid's Corner nav(只有 kid 連結) -->
{% else %}
  <!-- Rex's Hub 完整 nav -->
{% endif %}

為什麼這樣就夠:Jekyll 在伺服器端靜態產生 HTML,sidebar 只在完整頁面載入時渲染一次。因為 Kid sidebar 裡完全沒有主站連結,小孩透過 SPA 點任何連結都只在 /kid/ 內移動,自然看不到大人內容。只要小孩書籤直接指向 /kid/,效果完美。

效率做法(5 min)

  1. 只改 _includes/sidebar.html 一個檔
  2. 把原本整個 <nav> 包進 {% else %}...{% endif %}
  3. {% if %} 區塊內貼 Kid 版 nav(從 else 區塊的 kid <details> 群組複製,升為頂層 group)
  4. _config.ymldefaults: kid/ → layout: default(新頁面不用手動設 layout)

Bug:SPA 只攔截 .sidebar-nav 的點擊,頁面 .main-content 內的連結(如 ↩ Sha Dashboard)點下去觸發全頁重載 → Jekyll 重新渲染 → sidebar 切成 Kid’s Corner → 回不去 Rex’s Hub。

解法default.html 的 SPA init block,補一個 .main-content 的事件委派 listener:

// DOMContentLoaded 內,緊接 sidebar listener 之後
var mainEl = document.querySelector('.main-content');
if (mainEl) {
  mainEl.addEventListener('click', function (e) {
    var a = e.target.closest('a[href]');
    if (!a || !isLocal(a.getAttribute('href'))) return;
    e.preventDefault();
    navigate(a.getAttribute('href'));
  });
}

關鍵細節:listener 掛在 .main-content 容器(而非個別 <a>)→ 事件委派,SPA 換頁後 innerHTML 被替換也不需重新綁定,新插入的連結全部自動繼承攔截。

效率做法(3 min):未來建站第一天就把這段加進去,和 sidebar 的 listener 並排,一次到位。


3. Service Worker — MP3 零延遲 / 零重複流量 / 離線播放

需求:539 個短 mp3(TOEIC 100 + Kid 439,均約 9 KB,合計 4.6 MB),手機重播時省流量、首播零延遲(快取後)、支援離線。

實作:新增 sw.js(repo 根目錄),default.html <head> 一次性註冊(全站生效)。

策略

| 資源 | 策略 | 說明 | |——|——|——| | .mp3 | Cache First + Range 切片 + LRU | 命中直接回傳,零流量 | | HTML / JS / CSS | Stale-While-Revalidate | 有快取先回傳,背景更新 |

踩過的兩個坑(最重要)

坑 1:全部 ❌ — Range Request 瀏覽器播放 <audio> 時幾乎必定發出 Range: bytes=X-Y(HTTP 範圍請求)。SW 若直接從快取回傳完整 200 OK,瀏覽器期望 206 Partial Content,格式不符 → 全部播放失敗。

修法:偵測 Range header → 手動 ArrayBuffer.slice() 切片 → 回傳正確 206:

function makeRangeResponse(buffer, contentType, rangeHeader) {
  const total = buffer.byteLength;
  const m     = /bytes=(\d+)-(\d*)/.exec(rangeHeader);
  const start = parseInt(m[1], 10);
  const end   = m[2] ? parseInt(m[2], 10) : total - 1;
  return new Response(buffer.slice(start, end + 1), {
    status: 206,
    headers: {
      'Content-Type'  : contentType,
      'Content-Range' : `bytes ${start}-${end}/${total}`,
      'Content-Length': String(end - start + 1),
      'Accept-Ranges' : 'bytes',
    }
  });
}

坑 2:部分 ❌ — response.clone() 雙重使用 cache.put(key, response.clone()) 之後,再在 serveRange() 裡對同一個 response 呼叫第二次 .clone(),在部分瀏覽器的 Streams 實作中,body stream 進入 disturbed 狀態 → arrayBuffer() 失敗。

修法:body 只讀一次,fork 成兩份用途:

const buffer = await fullResp.arrayBuffer();          // 讀一次
cache.put(key, new Response(buffer.slice(0), {...})); // cache 用獨立 copy
return makeRangeResponse(buffer, contentType, rangeHdr); // serve 用原 buffer

各頁面加 clear cache checkbox

6 個單字版型(toeic_wordskid_wordstepi06_wordstepi07_wordstoeic_phrasetoeic_reading)的 .pa-bar 都加:

<label class="pa-chk-label pa-chk-cache">
  <input type="checkbox" id="clearcache-chk"> clear cache
</label>

勾起 → 清除 mp3-* 快取桶 → 自動取消勾選(一次性)。Mac 上幾乎瞬間完成,看起來像「勾不上去」,屬正常。

未來維護

效率做法(未來建新站 30 min)

  1. 直接複製本站 sw.js,只改 CACHE_VERSION
  2. default.html <head> 貼入 SW 註冊(8 行)
  3. 各版型 .pa-bar 貼 clear cache label(1 行)
  4. play-lib.html 貼 clear cache JS + CSS(約 25 行)
  5. 絕對不能省略 makeRangeResponse,這是音訊 SW 的最大坑

相關連結