Atom
本站架站紀錄_第四篇

本站架站紀錄_第四篇

新增今日天氣、歷史上的今天、留言區。

加入今日天氣

看到別人有加我也想說加個,給自己的blog加點小東西
這邊我們使用Free Weather API
可以自己選擇地點顯示當地天氣
新增一個layout/_widget/weather.ejs因為東西要放在側邊欄

  • weather.ejs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <div layout/_widget/weather.ejs
    class="kira-widget-wrap card-weather" data-lat="25.0375" data-lon="121.5637" data-city="台北">
    <h3 class="kira-widget-title">
    <i class="kirafont icon-weather"></i>
    台北 · 今日天氣
    </h3>

    <div class="weather-body">
    <div class="w-top">
    <div class="w-left">
    <div class="w-temp"><span id="w-now">--</span><span class="unit">°C</span></div>
    <div class="w-hilo">H <span id="w-max">--</span>° · L <span id="w-min">--</span>°</div>
    </div>
    <div class="w-right">
    <div id="w-icon" class="w-icon">⛅</div>
    <div id="w-desc" class="w-desc">--</div>
    <div id="w-time" class="w-time">--:--</div>
    </div>
    </div>

    <!-- 兩條長膠囊:降雨、濕度 -->
    <div class="w-grid chips-wide">
    <div class="chip">
    <span class="label">降雨</span>
    <span class="value"><span id="w-pop">--</span><span class="unit">%</span></span>
    </div>
    <div class="chip">
    <span class="label">濕度</span>
    <span class="value"><span id="w-rh">--</span><span class="unit">%</span></span>
    </div>
    </div>
    </div>
    </div>
    然後是美化的部分
  • source/weather-card.css
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    .kira-right-column .kira-widget-wrap.card-weather{ width:100%; }

    /* 玻璃感卡片容器 */
    .card-weather .weather-body{
    border-radius:16px; background: linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.04));
    border:1px solid var(--card-border, rgba(255,255,255,.18)); box-shadow:0 10px 22px rgba(0,0,0,.06);
    padding:14px 16px;
    }

    /* 頂部:左大字溫度、右側圖示/描述/時間 */
    .card-weather .w-top{ display:flex; align-items:center; justify-content:space-between; gap:12px; }
    .card-weather .w-left{ display:flex; flex-direction:column; gap:4px; }
    .card-weather .w-temp{ font-weight:900; line-height:1; letter-spacing:.5px; }
    .card-weather .w-temp #w-now{ font-size:36px; }
    .card-weather .w-temp .unit{ font-size:16px; opacity:.85; margin-left:2px; }
    .card-weather .w-hilo{ opacity:.9 }
    .card-weather .w-right{ display:flex; flex-direction:column; align-items:flex-end; gap:2px; text-align:right; }
    .card-weather .w-icon{ font-size:28px; line-height:1; }
    .card-weather .w-desc{ font-size:.95rem; opacity:.95; }
    .card-weather .w-time{ font-size:.85rem; opacity:.7 }

    /* 兩條長膠囊(單欄) */
    .card-weather .w-grid.chips-wide{ display:grid; grid-template-columns: 1fr; row-gap:8px; margin-top:12px; }
    .card-weather .chip{ display:flex; align-items:center; justify-content:space-between; gap:12px;
    border-radius:12px; padding:10px 12px; background: rgba(255,255,255,.10);
    border:1px solid var(--card-border, rgba(255,255,255,.18)); font-size:.95rem; line-height:1; }
    .card-weather .chip .label{ font-weight:600; letter-spacing:.3px; white-space:nowrap; opacity:.85; }
    .card-weather .chip .value{ font-weight:800; font-variant-numeric: tabular-nums; white-space:nowrap; display:inline-flex; align-items:baseline; gap:4px; }

    @media (prefers-color-scheme: light){
    .card-weather .weather-body{ background:#fff; border-color:rgba(0,0,0,.08); box-shadow:0 10px 22px rgba(0,0,0,.05); }
    .card-weather .chip{ background:#f7f9fb; border-color:rgba(0,0,0,.06); }
    }

最後是獲取天氣資料的腳本

  • source/js/weather-card.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    (function () {
    const box = document.querySelector('.kira-widget-wrap.card-weather');
    if (!box) return;

    const lat = parseFloat(box.dataset.lat || '25.0375');
    const lon = parseFloat(box.dataset.lon || '121.5637');

    const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}`
    + `&current=temperature_2m,precipitation,relative_humidity_2m,weather_code,wind_speed_10m`
    + `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,sunrise,sunset`
    + `&timezone=Asia%2FTaipei`;

    const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };

    const codeMap = (code, isNight) => {
    const sun='☀️', moon='🌙', cloud='☁️', pcloud='⛅', rain='🌧️', lrain='🌦️', tstorm='⛈️', snow='❄️', fog='🌫️';
    const txt = {0:'晴朗',1:'多雲時晴',2:'多雲',3:'陰',45:'霧',48:'霧凇',51:'毛毛雨',53:'小雨',55:'中雨',56:'凍雨(毛毛雨)',57:'凍雨',61:'小雨',63:'中雨',65:'大雨',66:'凍雨',67:'強凍雨',71:'小雪',73:'中雪',75:'大雪',77:'霰',80:'陣雨',81:'強陣雨',82:'暴陣雨',85:'陣雪',86:'強陣雪',95:'雷雨',96:'雷雨雹',99:'劇烈雷雨雹'};
    const emoji = code===0 ? (isNight?moon:sun) : [1,2].includes(code)?pcloud : code===3?cloud :
    [51,53,55,61,63,65,80,81,82].includes(code) ? (code===51?lrain:rain) :
    [95,96,99].includes(code) ? tstorm : [71,73,75,77,85,86].includes(code)?snow : [45,48].includes(code)?fog : cloud;
    return { emoji, text: txt[code] || '—' };
    };

    fetch(url).then(r=>r.json()).then(d=>{
    const nowISO = d?.current?.time || new Date().toISOString();
    const now = new Date(nowISO);
    const sunrise = new Date(d?.daily?.sunrise?.[0] || now);
    const sunset = new Date(d?.daily?.sunset?.[0] || now);
    const isNight = now < sunrise || now > sunset;

    const nowT = Math.round(d?.current?.temperature_2m ?? NaN);
    const rh = d?.current?.relative_humidity_2m ?? null;
    const wcode= d?.current?.weather_code ?? 3;

    const maxT = Math.round(d?.daily?.temperature_2m_max?.[0] ?? NaN);
    const minT = Math.round(d?.daily?.temperature_2m_min?.[0] ?? NaN);
    const pop = d?.daily?.precipitation_probability_max?.[0];

    if (Number.isFinite(nowT)) set('w-now', nowT);
    if (Number.isFinite(maxT)) set('w-max', maxT);
    if (Number.isFinite(minT)) set('w-min', minT);
    if (typeof pop === 'number') set('w-pop', pop);
    if (typeof rh === 'number') set('w-rh', rh);

    const meta = codeMap(wcode, isNight);
    set('w-icon', meta.emoji); set('w-desc', meta.text);
    set('w-time', now.toLocaleTimeString('zh-TW',{hour:'2-digit',minute:'2-digit'}));
    }).catch(()=>{
    box.querySelector('.weather-body').innerHTML = '<div style="opacity:.85">天氣資料載入失敗,稍後再試</div>';
    });
    })();

最後一樣,記得載入到header。

1
2
<%- css('weather-card.css') %>
<script src="<%- url_for('/js/weather-card.js') %>" defer></script>

歷史上的今天

在這邊我們使用維基百科
除了歷史上的今天外你也可以改成顯示在維基上你有興趣的東西

  • 修改側邊欄

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <div class="kira-widget-wrap card-history">
    <h3 class="kira-widget-title">
    <i class="kirafont icon-time"></i>
    歷史上的今天
    </h3>


    <!-- 只固定內容區高度,外層不要鎖高避免切到標題 -->
    <div class="history-card">
    <div class="swiper" id="history-container" aria-live="off">
    <div class="swiper-wrapper" id="history_container_wrapper"></div>
    </div>
    <span class="swiper-notification" aria-live="assertive" aria-atomic="true"></span>
    </div>
    </div>
  • 修改樣式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    :root{
    --history-h: 132px; /* 內容區高度:120~160 皆可 */
    --history-lines: 3; /* 每則顯示行數 */
    }


    .kira-right-column .kira-widget-wrap.card-history{ width:100%; }


    /* 標題列沿用主題色,這裡只控間距與圖示大小 */
    .kira-widget-wrap.card-history .kira-widget-title{
    display:flex; align-items:center; gap:.5rem; margin:0 0 12px;
    }
    .kira-widget-wrap.card-history .kira-widget-title .kirafont{ font-size:1.05em; }


    /* 固定內容高度,避免被長文字撐爆;左右留白 14px 看起來不擠 */
    .kira-widget-wrap.card-history .history-card{ height:var(--history-h); overflow:hidden; }
    .kira-widget-wrap.card-history .swiper{ width:100%; height:var(--history-h); }
    #history_container_wrapper{ height:100% !important; }
    .kira-widget-wrap.card-history .swiper-slide{
    height:var(--history-h); padding:8px 14px; box-sizing:border-box; position:relative;
    }


    /* 多行顯示 + 截斷(避免爆版);年份微強調 */
    .kira-widget-wrap.card-history .history-year{ font-weight:700; letter-spacing:.5px; opacity:.85; }
    .kira-widget-wrap.card-history .history-text{
    white-space:normal; overflow:hidden; display:-webkit-box;
    -webkit-box-orient: vertical; -webkit-line-clamp: var(--history-lines);
    line-height:1.45; font-size:.95rem;
    }


    /* 分隔線用偽元素,不改變高度,翻頁更準 */
    .kira-widget-wrap.card-history .swiper-slide::after{
    content:""; position:absolute; left:0; right:0; bottom:0; height:1px;
    background: var(--card-border, rgba(255,255,255,.15)); pointer-events:none;
    }
    @media (prefers-color-scheme: light){
    .kira-widget-wrap.card-history .swiper-slide::after{ background: rgba(0,0,0,.08); }
    }
  • 最後是獲取資料的腳本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    /* Wikipedia On-This-Day — 手動翻頁、固定高度版 */
    (function () {
    const wrap = document.getElementById('history_container_wrapper');
    if (!wrap) return;


    const lang = 'zh';
    const today = new Date();
    const mm = String(today.getMonth() + 1).padStart(2, '0');
    const dd = String(today.getDate()).padStart(2, '0');
    const feedUrl = `https://api.wikimedia.org/feed/v1/wikipedia/${lang}/onthisday/all/${mm}/${dd}`;
    const cacheKey = `HISTORY_TODAY_${lang}_${mm}${dd}`;


    const render = (list) => {
    wrap.innerHTML = '';
    list.forEach(item => {
    const slide = document.createElement('div');
    slide.className = 'swiper-slide';
    slide.innerHTML = `
    <div class="history-year">A.D.${item.year}</div>
    <div class="history-text">${item.text}</div>`;
    if (item.link) slide.addEventListener('click', () => window.open(item.link, '_blank'));
    wrap.appendChild(slide);
    });
    };

    const createSwiper = () => {
    if (window.__historySwiper__) window.__historySwiper__.destroy(true, true);
    window.__historySwiper__ = new Swiper('#history-container', {
    direction: 'vertical', effect: 'slide', slidesPerView: 1, spaceBetween: 0,
    loop: false, centeredSlides: false, speed: 350, resistanceRatio: 0,
    allowTouchMove: true,
    mousewheel: { forceToAxis: true, releaseOnEdges: true, sensitivity: .7 },
    keyboard: { enabled: true, onlyInViewport: true }
    });
    };

    const run = list => { render(list); createSwiper(); };

    // 快取先行
    try { const cache = JSON.parse(localStorage.getItem(cacheKey) || 'null'); if (cache?.length) run(cache); } catch{}

    // 再抓 API 更新
    fetch(feedUrl).then(r=>r.json()).then(data => {
    const events = Array.isArray(data?.events) ? data.events : [];
    const list = events.slice(0, 15).map(ev => {
    const p = (ev.pages && ev.pages[0]) || {};
    const link = p?.content_urls?.desktop?.page || p?.content_urls?.mobile?.page || '';
    return { year: ev.year, text: ev.text, link };
    });
    localStorage.setItem(cacheKey, JSON.stringify(list));
    run(list);
    }).catch(() => run([{ year: '—', text: '載入失敗,請稍後再試', link: '' }]));
    })();
  • 同樣,別忘了載入到頁面上

    1
    2
    3
    4
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
    <%- css('css/history-today.css') %>
    <script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js" defer></script>
    <script src="<%- url_for('/js/history-today.js') %>" defer></script>

備註:頁面大小樣式等都是慢慢修改到我喜歡的大小的,這方面有要修改就再自己手動測試吧。

留言區

Kira主題中本身就有寫好的giscus跟gitalk可以直接用了
一開始我也是直接開來用
但使用這兩個需要登入github,所以我想改成不需要登入也能留言的方式,自由度比較高
最後我使用[Twikoo](https://github.com/twikoojs/twikoo)

免費搭建、簡單部屬
留言存放區我使用MongoDB,畢竟使用度不高沒人會留言所以免費額度就夠了

步驟1

  1. 在 GitHub 建立一個新的 repo,例如:twikoo-vercel
    結構如下:
    1
    2
    3
    4
    5
    twikoo-vercel/
    ├─ api/
    │ └─ index.js
    ├─ package.json
    └─ vercel.json
  2. api/index.js
    1
    2
    3
    4
    5
    import twikoo from 'twikoo-vercel'

    export default async function handler(req, res) {
    return twikoo(req, res)
    }
  3. package.json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "name": "twikoo-vercel",
    "version": "1.0.0",
    "type": "module",
    "dependencies": {
    "twikoo-vercel": "^1.6.18"
    },
    "engines": {
    "node": "18.x"
    }
    }
  4. vercel.json
    1
    2
    3
    4
    5
    6
    {
    "version": 2,
    "builds": [
    { "src": "api/index.js", "use": "@vercel/node" }
    ]
    }

將這個repo部屬到Vercel
成功後就能拿到api網址

步驟2

建立 MongoDB Atlas Database

  1. 註冊 MongoDB Atlas
  2. 建立一個免費的 Cluster。
  3. 在 Database Access 新增一個使用者:
    • 使用者名稱:twikooUser
    • 權限:Atlas Admin 或 Read and write to any database
  4. 在 Network Access 加入 IP 白名單:
    • 設定 0.0.0.0/0(允許所有 IP)。
    • 這樣 Vercel 才能連上 Atlas
  5. 複製連線字串(Node.js Driver → 4.0+):
    1
    mongodb+srv://twikooUser:yourPassword@cluster0.xxxxx.mongodb.net/twikoo?retryWrites=true&w=majority

步驟3

  1. 到 Vercel → twikoo-vercel → Settings → Environment Variables。
  2. 新增一個變數:
    • Key:MONGODB_URI
    • Value:剛剛複製的 MongoDB 連線字串
  3. 儲存並 Redeploy。

步驟4

  1. 在主題設定檔 themes/kira/_config.yml 新增:
    1
    2
    3
    4
    5
    6
    comments:
    use: twikoo

    twikoo:
    envId: https://twikoo-vercel-xxxx.vercel.app/api
    lang: zh-TW
  2. 在主題的 layout/components/comments/ 新增一個 twikoo.ejs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div id="tcomment"></div>
    <script src="https://cdn.jsdelivr.net/npm/twikoo/dist/twikoo.all.min.js"></script>
    <script>
    twikoo.init({
    envId: '<%= theme.twikoo.envId %>',
    el: '#tcomment',
    lang: '<%= theme.twikoo.lang || "zh-TW" %>'
    })
    </script>
  3. 在post中加入twikoo的選項
    1
    2
    3
    4
    5
    <div class="kira-post-footer">
    <%- partial('components/comments/gitalk') %>
    <%- partial('components/comments/giscus') %>
    <%- partial('components/comments/twikoo') %>
    </div>

記得存檔,如果之後有更新比較多東西才會再繼續寫。

本文作者:Atom
本文鏈接:https://d0ngd.github.io/2025/10/08/本站架站紀錄_第四篇/
版權聲明:本文採用 CC BY-NC-SA 3.0 CN 協議進行許可