今天要解的問題

Day21 處理了手機上固定列太多的問題,也在地點總表的「吃」底下加了子分類。這次一打開手機,我就發現一件更根本的事:子分類按鈕做在「行程紀錄」的地點總表裡,但我平常看的是「行前指南」。

我對 Claude 說:「我沒有看到 sub 的選項。」它去查了之後回我:店家資料其實有兩份——外層頁面的地點總表,和內嵌行前指南自己的地區清單。指南是 Day19 原樣嵌進來的,之後新加的店都寫進了總表,指南裡一家都沒有。

這讓我重新想了一次這個頁面要幫我做什麼。旅途中我需要的其實是這四件事:

  1. 少跳轉:店家、每日建議、注意事項都在同一頁,點了就到,不用在分頁和網頁之間切來切去
  2. 每天打開就有當天的建議行程
  3. 旅途中同步已完成的行程:去了哪家店就記回指南,之後問「附近吃什麼」時先列還沒去過的;回來後的整理也直接在同一份資料上做
  4. 頁首講清楚還剩多少時間:出發前還有幾天、旅途中是第幾天

所以主線是把行前指南升成整頁的主角,導覽、資料、同步流程、頁首都跟著改。支線有兩條:每家店都畫進示意圖(寫了一支依地址補座標的腳本),以及備註頁的目錄與排序。

流程圖,分成三欄。出發前(頁首:還有 N 天):/trip-new 開一趟新旅行,產生行前指南(地區|建議行程|備註),加店家後跑 geocode.mjs 補座標畫進示意圖。旅途中(頁首:第 N 天),每天重複:打開就是建議行程的今天,點選項跳到地區的店家卡片與示意圖;/trip-log「剛去了 X」把紀錄的當天項目改成去了、指南裡那家店標上去過;/nearby「附近吃什麼」先列指南裡還沒去過的店。回來後(頁首:只留起訖):/trip-wrap 把還是預定或待填的項目一次問完,紀錄就是實際走過的行程,最後更新旅行總覽。圖例:實線綠框是旅行頁上看得到的,虛線框是在 Claude Code 裡下的指令

頁面負責「看」,Claude Code 的指令負責「改」。這張圖裡的每一格,都是這次改過的。

這一趟這次照實寫城市和店名(釜山),但不寫出發日期與航班。截圖裡的日期一律換成範例日期,其他日期與航班做了模糊處理。

想法與取捨

誰是主角:紀錄,還是指南

Day19 的設計是「一頁走完一趟旅行」,以回來後的紀錄為主:每日實際行程、待填空格、學到的事、下次清單、地點總表,行前指南當成其中一個分頁原樣保留。

Claude 第一版的計畫還是照這個方向:把指南的分頁改成頁首的一個下拉選單,子分類補進指南。我直接否決了:

錯誤,行前指南才是我主要的項目,請把指南升級成真正的行程助手,行程紀錄優先度請往後退,並將其中的資訊都塞入到指南,行程紀錄只保留實際行程,連下面的學到的事、填表都移除掉。

模板是照「理論上該有什麼」設計的,我實際用的只有指南。 旅途中打開頁面,我要的是今天去哪、附近吃什麼、注意什麼,這些都在指南裡;待填空格和學到的事,是回來後才會碰的東西,而且一直沒填過。

所以這次拿掉的東西:大標題、數字儀表板、待填空格、學到的事、下次清單、地點總表。紀錄頁只剩每日行程,以及一行訂位資訊的小字。

這有代價:待填空格是存在手機瀏覽器裡的,如果之前填過東西,新版就看不到了。Claude 在動手前把這點列出來,提醒先在舊頁面按「複製全部」備份。

指南的分項:下拉選單,還是攤平成頁首導覽

原本的結構是兩層:外層頁面有「行程紀錄|行前指南」,指南裡面又有一列「地區|行程推薦|備註」。Day21 已經處理過兩層疊在一起很卡的問題,但那是用「往下滑就收起」繞過去,層級本身還在。

Claude 的計畫是在外層的「行前指南」按鈕上加一個 ▾,點開是下拉選單列出三個分項。我回的是:

將指南內的選項們都往上拆一層,地區/行程推薦/備註,將行程紀錄改成紀錄,與這些合成一塊,上面有四項,手機要變成漢堡選單,避免點不到。

下拉選單還是多一層:要先點「行前指南」,再點分項。攤平之後,頁首就是 地區|建議行程|備註|紀錄 四項(「行程推薦」後來也被我改名成「建議行程」)。

手機上四項排不下,也不想讓它變成左右滑動的一列,所以改成漢堡選單:右上角一顆按鈕寫著目前在哪一項,點開是直向的大字清單,每列 54px 高。

難的地方是:前三項其實不在外層頁面裡。 指南是放在沙箱 iframe 裡的另一份文件,它的分頁按鈕、切換邏輯都在 iframe 裡面,外層碰不到。Claude 的做法是兩邊各做一半,見「實作」。

資料放哪:外層傳給指南,還是直接寫進指南

把地點總表「塞進指南」有兩種做法,Claude 列出來問我:

  • 直接寫進指南的地區清單:指南成為唯一的店家資料來源;代價是 /nearby、/trip-log 這些指令以後要先把指南從頁面取出、改完再放回去
  • 資料留在外層,指南載入時由外層傳進去:指令改動少;代價是單獨打開指南時看不到這些店

Claude 推薦第一個,我也選了第一個。

併進去時有一個限制:指南的 ★ 標記是用「第幾區的第幾家」當 id 存的。新的店只能加在每一區清單的最後面,插在中間,之後所有標記都會對錯店。所以總表裡 20 家指南沒有的店,都加在各區的尾端;原本就在指南裡的 25 家,只補上子分類、標籤和心得。

這條規則也寫進了給 Claude 讀的規則檔,以後新增店家都照這樣做。

旅途中記下的東西:寫進紀錄就好,還是也寫回指南

旅途中我不改頁面,是在 Claude Code 裡講一句「剛去了 X」,由 /trip-log 這個指令去改。改版前,這個指令的規則檔最後一行寫的是:

行前指南分頁保持原樣,不因實際行程而修改

指南是出發前的計畫,紀錄是實際發生的事,兩邊分開。可是這樣一來,指南永遠不知道我去過哪裡:旅途中第三天打開「地區」,看到的還是出發前的那份清單。

這次把那一行拿掉,/trip-log 改成同時寫兩個地方:

  • 紀錄:當天的預定項目改成「去了」,店名與心得照實寫
  • 指南:那家店標上 v:true,卡片上出現「去過」,心得連同日期寫進 note

連帶改了另外兩個指令:

  • /nearby(「附近吃什麼」)原本是從地點總表找沒去過的店,現在改成從指南的地區清單裡找 v 不是 true 的,先列還沒去過的
  • /trip-wrap(回來後收尾)原本要整理待填空格、學到的事、下次清單;這些都拿掉了,只剩把還是「預定」「待填」的項目一次問完,再把指南裡的店標好去過/沒去

所以指南不再是出發前的快照,而是會跟著旅程更新的那一份。

頁首的日期:獨立一層,還是併進原本那一列

第四件事是頁首要講清楚還剩多少時間。我想在頁首放這趟旅行的日期,要求是:「中間放目前的日期,若是第一天則左側時間特別,最後一天則是右側時間特別。」

第一版 Claude 在導覽列上方多加了一列日期,旅途中還會再多一列「今天的提醒」。我看了之後說:

我要的不是多一個 header,而是整合進去目前的 header,不需要再多一個 header layer。

Day21 才花了力氣把三層固定列壓下來,這裡又疊了兩層回去。改成日期放在導覽同一列的右邊;今天的提醒那一列拿掉,當天的注意事項本來就寫在建議行程與紀錄的當天卡片上。

接著我又補了兩個規則:

如果是時間還沒到的現在,要在左邊以外。已經結束的,則不需要再追蹤,直接保留起訖日期就好了。

階段頁首顯示
出發前今天|出發|回程(今天在出發日左邊)
旅途中出發|今天|回程
結束後出發|回程

「今天」不再固定在中間,而是放在時間軸上它真正的位置:還沒到出發日,就在出發日的左邊。

日期的樣式:框框之外,讓我自己選

第一版的「今天」是一個圓角框,出發或回程當天,那一端會加上底色。我說「起訖時間不要用框框,請給我選項來決定」。

Claude 給了四個選項,每個附一段文字示意:

  • 箭頭串起來的純文字
  • 時間軸細線:日期是線上的小圓點,今天是實心綠點
  • 純文字,強調的那個加底線
  • 只用字色與粗細區分

我選了時間軸。之後又補了兩個細節:日期要帶星期;出發前,今天那個點到出發日之間用虛線(還沒走到那條線上)。

頁首日期的三種狀態,用模板範例日期:出發前是左邊一個實心綠點寫「還有 N 天」,虛線連到出發日的空心圓,再實線連到回程;旅途中是出發與回程兩個空心圓之間有一個實心綠點寫「第 3 天」;結束後只剩出發與回程兩個空心圓和一條實線

另外有一件我自己沒想到、Claude 提出來的事:網站判斷「今天」用的是台北時間,但韓國快一小時。旅途中台北晚上 11 點到 12 點,韓國已經過了午夜,頁面卻還停在前一天。所以資料區多了一個 tz,旅途中用目的地的時區判斷今天。

備註頁:目錄怎麼放,內容怎麼排

指南的備註頁很長:簽證、Pass、交通、錢、吃飯、天氣……Claude 在最上方加了區塊目錄,桌機上是一排按鈕。手機上它原本寫的是「可以左右滑動」,我只回了一句:

手機版左右滑動??

改成跟導覽一樣的做法:手機上只留一條「目錄 目前那一節 ▾」,點開是直向清單。這條「手機上不要左右滑動的列」後來也寫進了規則檔和 Claude 的記憶裡。

排序則是我要求「從行程前的準備到行程中與後」。Claude 把每一節標上階段(出發前/旅途中/回程),目錄也照階段分組。原本「錢、免稅與退稅」混了兩個時間點的事,拆成旅途中的「錢與換錢」和回程的「到機場、退稅與免稅提領」。

這一輪我順便請它補了三樣內容:簽證與入境、可勾選的必帶物品清單、溫度對照衣服。簽證和入境的部分,它另外派了一個代理去查最新規定,查到彼此矛盾的來源(例如紙本入境卡還收不收),就照實寫「說法不一」,建議先填電子版。

示意圖:只畫有座標的店,還是每家都畫

這條是 PR 快結束時冒出來的。我在手機上篩選類別,發現:

點選類別後,下面項目的編號會不見

Claude 查到的原因是:示意圖只畫有座標的店,編號也只有畫在圖上的店才有。從地點總表併進指南的 20 家都沒有座標,所以沒有編號,篩選後看起來就像編號消失了。

最直接的做法是請 Claude 一家一家查座標寫進去,但之後旅途中每加一家店都要再來一次。所以它改成寫一支腳本 scripts/geocode.mjs:店家多記一個 addr(當地語言的路名地址),腳本去查座標寫回去,示意圖是頁面打開時依座標即時畫的,不用另外重畫。/nearby、/trip-log、/trip-new 都加上了「跑一次補座標」的步驟。

腳本有兩條規則,都是為了跑幾次結果都一樣:

  • 已經有座標的店一律不動,只補缺的。改完重跑一次,檔案完全沒變
  • 查到的點離該地區中心超過 6 公里就不採用,視為查錯,那家店維持沒有座標

剩下沒畫上去的店怎麼辦,Claude 第一版是編號一律顯示、不在圖上的用虛線圈。我接著要求:

不在圖上的也放在示意圖上面,但整體字用成斜線

所以現在每家店都在圖上,分三種:

  • 有座標:照實際位置畫
  • 太遠的(例如要轉車的景點):照原比例畫的話整張圖會縮到看不清楚,所以只依實際方向貼在圖的邊緣,虛線圈加斜體,清單寫「圖外・需轉車」
  • 沒有座標的:放在地區中心附近,清單寫「大約位置」

廣安里地區的示意圖:中間是地鐵站與 1 到 11 號店家的圓圈編號,外圍有 500m、1km、2km 的虛線同心圓;7 號五六島天空步道和 8 號荒嶺山觀景台是虛線圈,貼在圖的下緣與左緣。下方清單列出 11 家店的類別、名稱、最近的站和步行分鐘,7、8 號是斜體,寫著「圖外・需轉車」

實作

把 iframe 裡的分項,攤平到外層導覽

外層頁面不執行指南,只用 DOMParser 把它解析一次,讀出分項的名稱:

function buildDocTabs(){
  document.querySelectorAll('script[type="text/html"][data-doc]').forEach(src => {
    const id = src.dataset.doc.toLowerCase();
    DOCS[id] = src;
    const title = src.dataset.title || src.dataset.doc;
    // 只解析不執行,用來讀出分項名稱
    const parsed = new DOMParser().parseFromString(unescapeDoc(src.textContent), 'text/html');
    const tabs = [...parsed.querySelectorAll('[role=tablist] [role=tab]')].filter(t => t.id);
    if (tabs.length) {
      FIRST_TAB[id] = (tabs.find(t => t.getAttribute('aria-selected') === 'true') || tabs[0]).id;
      tabs.forEach(t => NAV.push({ p: id + ':' + t.id, label: t.textContent.trim(), doc: id, tab: t.id }));
    } else NAV.push({ p: id, label: title, doc: id });
    // ...

指南裡只要分項用的是標準的 [role=tablist] [role=tab],就會被攤平成頁首的項目,例如 guide:t-areas;沒有分項的文件維持一個按鈕。三個項目共用同一個 iframe。指南預設選中的那一項(建議行程)就是整頁的預設頁。

指南裡的「建議行程」也補上了一段:旅途中依目的地的時間找到今天那一天,加上「今天」的標記並直接選中。兩件事合起來,旅途中打開頁面,第一眼就是當天的建議行程。

iframe 裡注入一段 docTabsShim,跟 Day21 的 docScrollShim 放在一起:

function docTabsShim(id, initial){
  document.documentElement.classList.add('ts-tabs');
  const tabs = () => [...document.querySelectorAll('[role=tablist] [role=tab]')];
  const pick = t => {
    const b = tabs().find(x => x.id === t);
    if (b && b.getAttribute('aria-selected') !== 'true') b.click();
    return !!b;
  };
  addEventListener('message', e => {
    const m = e.data;
    if (e.source !== parent || !m || m.type !== 'ts-doc-tab' || typeof m.tab !== 'string') return;
    if (pick(m.tab)) scrollTo({ top: 0, behavior: 'instant' });   // 文件若設了平滑捲動,換分項時不要慢慢捲
  });
  document.addEventListener('DOMContentLoaded', () => {
    if (initial) pick(initial);
    let now = '';
    const report = () => {
      const b = tabs().find(x => x.getAttribute('aria-selected') === 'true');
      if (!b || b.id === now) return;
      now = b.id;
      parent.postMessage({ type: 'ts-doc-tab-now', doc: id, tab: b.id }, '*');
    };
    new MutationObserver(report).observe(document.body, { subtree: true, attributes: true, attributeFilter: ['aria-selected'] });
    report();
  });
}

它做三件事:

  • 在指南的 <html> 加上 ts-tabs,指南用這個 class 把自己那列分頁藏起來。單獨打開指南時沒有這個 class,分頁照舊
  • 外層點了某個分項,就送 ts-doc-tab 進來,shim 替使用者去點指南裡對應的那顆按鈕。切換本身完全用指南原本的程式
  • 用 MutationObserver 盯著 aria-selected,分項一變就回報。這是為了指南內部的跳轉:在建議行程點某家店,指南會自己切到「地區」,外層的導覽也要跟著亮到「地區」

外層收到回報時,第一次和之後的處理不同:

addEventListener('message', e => {
  const m = fromDoc(e, 'ts-doc-tab-now');
  if (!m || typeof m.tab !== 'string' || !NAV.some(n => n.p === m.doc + ':' + m.tab)) return;
  const first = !ready[m.doc];
  ready[m.doc] = true;
  // 剛載入時若使用者已經點了別的分項,補送一次;之後以文件回報的為準(例如從建議行程跳到地區)
  if (first && want[m.doc] && want[m.doc] !== m.tab) { sendTab(m.doc); return; }
  want[m.doc] = m.tab;
  if (!document.getElementById('p-' + m.doc).hidden) mark(m.doc + ':' + m.tab);
});

fromDoc 跟其他訊息共用同一套檢查:訊息必須來自那個分頁自己的 iframe。iframe 還在載入時,使用者可能已經點了別的分項,所以第一次回報如果跟想要的不一樣,就補送一次;之後一律以指南回報的為準。

頁首的時間軸

  const leg = (cls, label, date, info, hot) => `<div class="pt leg ${cls}${hot ? ' hot' : ''}"><i class="dot"></i>
    <b>${md(date)}<span class="w">(${wd(date)})</span></b><small>${info && info.length ? esc(info.join(' ')) : esc(label)}</small></div>`;
  // 還沒出發:今天到出發日之間用虛線
  const before = TODAY < TRIP.start;
  const start = leg(before ? 'cld cr' : 'cr', '出發', TRIP.start, TRIP.go, TODAY === TRIP.start);
  const end = leg('cl', '回程', TRIP.end, TRIP.back, TODAY === TRIP.end);
  const seg = '<i class="seg"></i>';
  const now = (sub, cls = '') => `<div class="pt now ${cls}"><i class="dot"></i><b>${todayMD}<span class="w">(${wd(TODAY)})</span></b><small>${sub}</small></div>`;
  // 出發/回程當天,今天就是那個端點,不另外畫一個點
  const mid = TODAY === TRIP.start || TODAY === TRIP.end ? '' : now(`第 ${dayDiff(TRIP.start, TODAY) + 1} 天`, 'cl cr') + seg;
  document.getElementById('dates').innerHTML =
    before ? now(`還有 ${dayDiff(TODAY, TRIP.start)} 天`, 'crd') + '<i class="seg dash"></i>' + start + seg + end
    : inTrip ? start + seg + mid + end
    : start + seg + end;

三種階段就是最後那個三元運算。cl/cr 標示這個點的左邊或右邊有線,cld/crd 是虛線版。出發或回程當天,今天就是那個端點,加上 hot 變成綠色,不另外畫一個點。

線為什麼要分成「點自己的半截」和「中間的線段」兩部分,見「踩到的坑」。

依地址補座標

geocode.mjs 先把內嵌的指南取出來,挑出要查的店:

  const todo = [];
  for (const area of AREAS) {
    const hub = area.hub || (area.st && area.st[0] && [area.st[0].lat, area.st[0].lng]);
    for (const it of area.items) {
      if (it.off || (Number.isFinite(it.lat) && Number.isFinite(it.lng))) continue;
      todo.push({ area, hub, it });
    }
  }

已經有座標的、標成太遠(off)的都跳過,所以重跑不會動到之前的結果。hub 是該地區的中心,沒有另外設就用第一個車站。

查詢用的是 OpenStreetMap 的 Nominatim,每秒最多查一次。查到之後先量距離:

    const query = it.addr || it.q;
    if (k) await sleep(1100);
    let hit = null, why = '';
    try { hit = await geocode(query); } catch (err) { why = err.message; }
    if (hit && hub) {
      const d = distKm(hub, [hit.lat, hit.lng]);
      if (d > maxKm) { why = `查到的點離「${area.short || area.name}」${d.toFixed(1)} 公里,太遠不採用(${hit.name.slice(0, 40)})`; hit = null; }
    }

有 addr 就用地址查,沒有才退回地圖搜尋用的 q。離地區中心超過 maxKm(預設 6)就當作查錯。採用的點寫回時會多一個 geo:'osm',看得出這個座標是腳本查的。

預設是預覽模式,只印出哪些查到、哪些查不到;加上 --write 才寫回頁面。

踩到的坑

時間軸的線碰不到點

第一版的時間軸,每個日期是一個欄位(點在上、日期文字在下),兩個日期之間放一條線段。在手機上看,線和點之間都有一段空隙。

原因是點置中在日期文字的上方,而日期文字比點寬得多。線段只能接到「欄位」的邊緣,接不到點。

改法是讓每個點自己畫半截線:左邊有線的點,用 ::before 從欄位左緣畫到中心;右邊有線的,用 ::after 從中心畫到右緣。中間的線段再把兩個欄位接起來,整條線就連續了:

.dates .pt.cl::before,.dates .pt.cr::after{content:"";position:absolute;top:4px;height:1px;background:var(--muted);opacity:.6;z-index:0}
.dates .pt.cl::before{left:0;right:50%}
.dates .pt.cr::after{left:50%;right:0}
.dates .dot{width:9px;height:9px;border-radius:50%;border:1.5px solid var(--muted);background:var(--paper);margin-bottom:3px;position:relative;z-index:1}

點用 z-index:1 壓在線上面,背景色跟頁面一樣,看起來就是線穿過空心圓。

手機上一列塞不下

日期併進導覽那一列之後,手機上要放:logo、三個日期、漢堡選單按鈕。第一次在 375px 寬的畫面上看,回程那一格被選單按鈕蓋住一半。

接著我又要求補上星期,日期變長,又蓋住了。最後的取捨:

  • 手機上,出發與回程只顯示日期與星期,時間和機場只在當天才顯示
  • 星期改用半形括號 (四),比全形 (四) 省兩個字寬
  • 日期字級縮小一級,線段最短縮到 6px

每改一次,都在手機尺寸實際看過才算數。這一列的寬度是被內容撐出來的,加一個字就可能爆掉。

合併時一刀切掉一個函式,整頁空白

這段期間主分支合進了 Day21 的「往上滑分兩段」,跟這次的改動在同一個檔案衝突。Claude 的解法是拿主分支的 docScrollShim 換掉這邊的舊版本,用的是「從 // 手機上回報捲動方向 這行開始,一直切到 function docHtml 為止」。

問題是 docTabsShim 剛好就放在這兩者之間,一起被切掉了。檢查腳本只檢查語法,沒有報錯;打開頁面是一片空白,console 裡是:

Uncaught ReferenceError: docTabsShim is not defined
    at docHtml
    at show

把 docTabsShim 從合併前的 commit 補回來就好了。用「從 A 切到 B」的方式替換程式碼時,要先確認 A 和 B 之間沒有別的東西。 這次是在瀏覽器裡實際打開才發現的,光跑檢查腳本看不出來。

記住的舊分頁,讓新的預設頁失效

改完之後第一次打開,頁面停在「紀錄」,而不是預設的建議行程。

頁面會記住上次看的分頁,存的 key 是 tab。我之前在舊版看過「行程紀錄」,存的值是 recap;新版的「紀錄」也叫 recap,是合法的值,所以就被沿用了。

改成用新的 key nav 存,舊的值自然就不會被讀到。結構改了,記住狀態的 key 也要跟著換,不然舊狀態會偷偷覆蓋新的預設。

切換分項時,畫面像是沒反應

從「地區」切到「行程推薦」時,截圖裡的畫面還停在一半的位置。shim 明明有呼叫 scrollTo(0, 0)。

指南的樣式裡有一條 html{scroll-behavior:smooth},所以 scrollTo 變成慢慢捲回頂端。在手機上,切了分項卻看到內容從中間慢慢往上滑,看起來像是切換卡住了。

改成明確指定 behavior: 'instant'。平滑捲動對「跳到某一家店」是好的,對「換一整個分項」不是。

手機的目錄清單分成兩欄

備註目錄在手機上展開時,清單右邊多出了被截斷的第二欄。原因是桌機版的目錄寫了 flex-wrap:wrap,手機版把它改成直向、又限制了最大高度,項目塞不下就換到第二欄去了。手機版補上 flex-wrap:nowrap 就好了。

只靠店名,幾乎查不到座標

腳本寫好後,先用預覽模式對那 20 家沒有座標的店跑一次:

查詢 20 筆:補上座標 2 筆,查不到 18 筆

  ✓ 西面|釜田市場  35.1607, 129.0614  ← 부전시장
  ✓ 西面|烤肉的男子  35.1571, 129.0615  ← 고기굽는남자 부산
  ✗ 西面|Diart Coffee La Lim  查不到  ← 디아트커피 라림 전포(沒有 addr,補上當地路名地址再跑一次)
  ✗ 西面|83獬豸(83 Haechi)  查不到  ← 83해치 서면(沒有 addr,補上當地路名地址再跑一次)
  ...

這時店家都還沒有 addr,查的是地圖搜尋用的店名字串,用的已經是韓文店名,還是幾乎全部落空。Claude 接著替這 20 家查了路名地址,只留路名和門牌、拿掉樓層,再跑一次:

查詢 20 筆:補上座標 19 筆,查不到 1 筆

查不到的那一家是換錢所,三個來源給了三個不同的地址,找不到可靠的,就先放「大約位置」。查地址的過程中還順便更正了一家換錢所的門牌:原本記的號碼其實是另一家換錢所。這類免費的地址查詢,餵它路名地址才有用,店名幾乎不行。 所以規則檔和 /nearby 都寫明:新增店家時一定要記 addr。

另外有一家冷麵店,我丟了一個 Google Maps 連結問 Claude 是不是這家。它解開連結後發現那是西面分店,頁面上記的卻是要搭車過去的本店。改成西面分店之後,它從「圖外・需轉車」變成西面示意圖上的一般點。以後店家位置不對,丟連結過去就好;直接取自連結的座標,在規則檔裡註明優先於腳本查到的結果。

小結

  • 少跳轉:頁首導覽是 地區|建議行程|備註|紀錄,前三項來自指南自己的分項;手機上是漢堡選單。地點總表的 20 家店併進指南各區的尾端,原本的 25 家補上子分類、標籤與心得,店家資料只剩一份
  • 每天的建議行程:預設頁是建議行程,旅途中依目的地時間直接選到今天
  • 旅途中同步:/trip-log 同時改紀錄與指南,/nearby 先列還沒去過的店,/trip-wrap 只剩確認去了沒;紀錄只留每日行程
  • 頁首倒數:一列裡放 logo、導覽、時間軸日期;出發前是「還有 N 天」、旅途中是「第 N 天」、結束後只留起訖,「今天」用目的地時區判斷
  • 示意圖:95 家店全部有編號、全部畫在圖上;geocode.mjs 依地址補了 19 家的座標,太遠和沒座標的用虛線圈
  • 備註:依出發前/旅途中/回程排序,有目錄;新增簽證與入境、必帶物品清單、溫度對照衣服
  • 模板與規則:外層模板、指南模板、檢查腳本、四個 Claude Code 指令、規則檔都改成以指南為主

本文同步發表於 kiwi-walk.com:https://kiwi-walk.com/blogs/engineer/ironman-2026-day22-guide-first/