Skip to content

Commit 9c29916

Browse files
committed
feat: add header button and clarify routing flow
1 parent b1738f0 commit 9c29916

16 files changed

Lines changed: 295 additions & 960 deletions

File tree

README.en.md

Lines changed: 0 additions & 942 deletions
This file was deleted.

astro.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ export default defineConfig({
2020

2121
base: "/",
2222

23+
// 新增語言轉換按鈕
24+
i18n: {
25+
defaultLocale: "zh-TW",
26+
locales: ["zh-TW", "en"], // 支援 繁體中文 和 英文
27+
routing: {
28+
prefixDefaultLocale: false, // 預設語言 (zh-TW) 不顯示 /zh-TW/
29+
},
30+
},
2331
markdown: {
2432
remarkPlugins: [remarkMath],
2533
rehypePlugins: [

src/components/Header.astro

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
MENU,
1010
BANNER,
1111
BANNER_SRCSET,
12+
Translation, // 新增這個區塊,專門控制翻譯功能
1213
TRIANGLE_BADGE,
1314
} from "../utils/config";
1415
import { urlFor } from "../utils/urlFor";
@@ -18,6 +19,7 @@ import RssBtn from "./partial/RssBtn";
1819
import SearchBtn from "./partial/SearchBtn";
1920
import ThemeBtn from "./partial/ThemeBtn";
2021
import SearchBox from "./partial/SearchBox";
22+
import TranslateBtn from "./partial/TranslateBtn"; // 新增語言轉換按鈕
2123
2224
type Props = {
2325
title?: string;
@@ -26,7 +28,9 @@ type Props = {
2628
2729
const { title = SITE.title, cover } = Astro.props;
2830
const posts = await getCollection("blog");
29-
const sortedPosts = posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
31+
const sortedPosts = posts.sort(
32+
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
33+
);
3034
---
3135

3236
<div id="header-nav">
@@ -50,6 +54,10 @@ const sortedPosts = posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDa
5054
<nav id="sub-nav" aria-label="Secondary navigation">
5155
<RssBtn client:load url={BASE_URL} className="nav-icon" />
5256
<SearchBtn client:load className="nav-icon" />
57+
58+
{/* 新增語言轉換按鈕,同時在 config.ts 設定可自由開關顯示 */}
59+
{Translation.enable && <TranslateBtn client:load className="nav-icon" />}
60+
5361
<ThemeBtn client:load className="nav-icon" />
5462
</nav>
5563
{

src/components/partial/ThemeBtn.tsx

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,34 @@ import {
88

99
import useTheme, { type Theme } from "../../hooks/useTheme";
1010

11+
// 定義多語言提示文字
12+
const I18N_TEXT = {
13+
"zh-TW": {
14+
toLight: "切換至淺色模式",
15+
toDark: "切換至深色模式",
16+
auto: "跟隨系統主題",
17+
},
18+
en: {
19+
toLight: "Switch to Light Mode",
20+
toDark: "Switch to Dark Mode",
21+
auto: "System Theme",
22+
},
23+
};
24+
1125
export default function ThemeToggle({ className }: { className?: string }) {
1226
const { theme, setTheme } = useTheme();
1327
const [isMounted, setIsMounted] = useState(false);
28+
const [lang, setLang] = useState<"zh-TW" | "en">("zh-TW"); // 預設語言為中文
1429

1530
useEffect(() => {
1631
setIsMounted(true);
32+
33+
// 用路徑判斷當前語言
34+
if (window.location.pathname.startsWith("/en")) {
35+
setLang("en");
36+
} else {
37+
setLang("zh-TW");
38+
}
1739
}, []);
1840
if (!isMounted) {
1941
return <></>;
@@ -26,7 +48,7 @@ export default function ThemeToggle({ className }: { className?: string }) {
2648
detail: {
2749
theme,
2850
},
29-
})
51+
}),
3052
);
3153
};
3254

@@ -40,17 +62,25 @@ export default function ThemeToggle({ className }: { className?: string }) {
4062
}
4163
};
4264

65+
const getTitle = () => {
66+
// 根據當前主題和語言返回對應的提示文字
67+
const t = I18N_TEXT[lang];
68+
69+
if (theme === "dark") return t.toLight; // 現在是暗的,提示按下去會變亮
70+
if (theme === "light") return t.toDark; // 現在是亮的,提示按下去會變暗
71+
return t.auto;
72+
};
73+
4374
return (
44-
<span
45-
className={className}
75+
<span
76+
className={className}
4677
onClick={handleClick}
78+
role="button" // 建議補上,增加可訪問性
79+
aria-label="Toggle Theme"
80+
title={getTitle()} // 根據當前主題動態設定提示文字
4781
>
48-
{theme === "dark" && (
49-
<FontAwesomeIcon icon={faMoon} scale={20} />
50-
)}
51-
{theme === "light" && (
52-
<FontAwesomeIcon icon={faSun} scale={20} />
53-
)}
82+
{theme === "dark" && <FontAwesomeIcon icon={faMoon} scale={20} />}
83+
{theme === "light" && <FontAwesomeIcon icon={faSun} scale={20} />}
5484
{theme === "auto" && (
5585
<FontAwesomeIcon icon={faCircleHalfStroke} scale={20} />
5686
)}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useState, useEffect } from "react";
2+
3+
interface Props {
4+
className?: string;
5+
}
6+
7+
export default function TranslateBtn({ className }: Props) {
8+
const [currentLang, setCurrentLang] = useState("zh-TW");
9+
10+
useEffect(() => {
11+
// 判斷當前語言
12+
if (window.location.pathname.startsWith("/en")) {
13+
setCurrentLang("en");
14+
} else {
15+
setCurrentLang("zh-TW");
16+
}
17+
}, []);
18+
19+
const toggleLanguage = () => {
20+
const currentPath = window.location.pathname;
21+
22+
// 1. 取得不含語言前綴的路徑 (把 /en 去掉)
23+
// 例如: "/en/about" -> "/about", "/en" -> "/"
24+
const basePath = currentPath.replace(/^\/en/, "") || "/";
25+
26+
let newPath = "";
27+
28+
if (currentLang === "zh-TW") {
29+
// 2. 切換到英文
30+
// 邏輯:加上 /en 前綴
31+
// 注意:如果 basePath 是 "/",則變為 "/en",否則為 "/en/about"
32+
newPath = `/en${basePath === "/" ? "" : basePath}`;
33+
} else {
34+
// 3. 切換到中文
35+
// 邏輯:直接使用 basePath (即移除 /en 之後的路徑)
36+
newPath = basePath;
37+
}
38+
39+
window.location.href = newPath;
40+
};
41+
42+
return (
43+
<div
44+
className={className}
45+
onClick={toggleLanguage}
46+
title={`Switch to ${currentLang === "zh-TW" ? "English" : "繁體中文"}`}
47+
role="button"
48+
aria-label="Switch Language"
49+
>
50+
<svg
51+
xmlns="http://www.w3.org/2000/svg"
52+
width="14"
53+
height="14"
54+
viewBox="0 0 640 640"
55+
fill="currentColor"
56+
>
57+
<path d="M192 64C209.7 64 224 78.3 224 96L224 128L352 128C369.7 128 384 142.3 384 160C384 177.7 369.7 192 352 192L342.4 192L334 215.1C317.6 260.3 292.9 301.6 261.8 337.1C276 345.9 290.8 353.7 306.2 360.6L356.6 383L418.8 243C423.9 231.4 435.4 224 448 224C460.6 224 472.1 231.4 477.2 243L605.2 531C612.4 547.2 605.1 566.1 589 573.2C572.9 580.3 553.9 573.1 546.8 557L526.8 512L369.3 512L349.3 557C342.1 573.2 323.2 580.4 307.1 573.2C291 566 283.7 547.1 290.9 531L330.7 441.5L280.3 419.1C257.3 408.9 235.3 396.7 214.5 382.7C193.2 399.9 169.9 414.9 145 427.4L110.3 444.6C94.5 452.5 75.3 446.1 67.4 430.3C59.5 414.5 65.9 395.3 81.7 387.4L116.2 370.1C132.5 361.9 148 352.4 162.6 341.8C148.8 329.1 135.8 315.4 123.7 300.9L113.6 288.7C102.3 275.1 104.1 254.9 117.7 243.6C131.3 232.3 151.5 234.1 162.8 247.7L173 259.9C184.5 273.8 197.1 286.7 210.4 298.6C237.9 268.2 259.6 232.5 273.9 193.2L274.4 192L64.1 192C46.3 192 32 177.7 32 160C32 142.3 46.3 128 64 128L160 128L160 96C160 78.3 174.3 64 192 64zM448 334.8L397.7 448L498.3 448L448 334.8z" />
58+
</svg>
59+
</div>
60+
);
61+
}

src/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import covers from "./covers";
33
export default {
44
site: {
55
title: "483's Blog",
6-
subtitle: "My Blog Subtitle",
6+
subtitle: "483 的小天地",
77
description: "483's blog",
88
keywords: "483, blog, astro, theme",
99
author: "483",
@@ -38,6 +38,11 @@ export default {
3838
],
3939
},
4040

41+
// 新增這個區塊,專門控制翻譯功能
42+
translation: {
43+
enable: true,
44+
},
45+
4146
footer: {
4247
since: 2026, // 2026 - current year
4348
powered: true,

src/covers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export default [
2-
"/images/SAO_2014.webp",
2+
// "/images/SAO_2014.webp",
33
"/images/Re0-765x372.webp",
44
"/images/NGNL-1048x645.webp",
55
"/images/GuiltyCrown-1440x900.webp",

src/pages/[...page].astro

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ type Props = {
3030
3131
export async function getStaticPaths({ paginate }: { paginate: Function }) {
3232
const posts = await getCollection("blog");
33-
const sortedPosts = posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
33+
const sortedPosts = posts.sort(
34+
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
35+
);
3436
return paginate(sortedPosts, {
3537
pageSize: 10,
3638
});
@@ -41,6 +43,7 @@ const { page } = Astro.props;
4143
<BaseLayout>
4244
<!-- Header -->
4345
<HeaderTitle title={SITE.title} url=`${BASE_URL}/` slot="header" />
46+
{/* slot 是 Astro 的俱名插槽,把 component 塞進指定 slot */}
4447
{
4548
SITE.subtitle && (
4649
<HeaderSubTitle
@@ -53,13 +56,19 @@ const { page } = Astro.props;
5356
<!-- Content -->
5457
<section id="main" aria-label="Main content">
5558
{
59+
// 非常重要,用條件渲染且引入元件的方式,實作 config 裡面 Home Categories 的開關
5660
HOME_CATEGORIES.enable && (
5761
<HomeCategories content={HOME_CATEGORIES.content} />
5862
)
5963
}
64+
{
65+
/* page.data 從 Astro.props 中解構出來,一頁有 pagesize 篇文章,
66+
用 map 把每一筆 post 轉換成 HTML 標籤 */
67+
}
6068
{
6169
page.data.map((post, index) => (
6270
<div class="post-wrapper">
71+
{/* 利用 { right: index % 2 === 1 } ,一個動態 class 來實作左圖右文、右圖左文 */}
6372
<div
6473
class:list={["post-wrap", { right: index % 2 === 1 }]}
6574
data-aos="fade-up"
@@ -70,6 +79,7 @@ const { page } = Astro.props;
7079
aria-label={post.id}
7180
/>
7281
<div class:list={["post-cover", { right: index % 2 === 1 }]}>
82+
{/* urlFor 定義在 utils ,保持模板 .astro 的乾淨 */}
7383
<img
7484
loading="lazy"
7585
src={urlFor(post.data.cover || randomCover() || BANNER)}
@@ -107,6 +117,7 @@ const { page } = Astro.props;
107117
</div>
108118
))
109119
}
120+
{/* 把 page 物件(父層)傳遞給子組件(pagination) */}
110121
<Pagination
111122
prevUrl={page.url.prev}
112123
nextUrl={page.url.next}

src/pages/archives/[...page].astro

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ import HeaderTitle from "../../components/partial/HeaderTitle.astro";
1111
import { BASE_URL } from "../../utils/config";
1212
import CategoryList from "../../components/partial/CategoryList.astro";
1313
14+
// 首頁使用 paginate 來產生分頁路由 (Astro 原生的魔法函式)
1415
export async function getStaticPaths({ paginate }: { paginate: Function }) {
1516
const posts = await getCollection("blog");
1617
posts.sort((a, b) => {
18+
// 以發佈日期排序,最新的在前面
1719
return b.data.pubDate.getTime() - a.data.pubDate.getTime();
1820
});
1921
return paginate(posts, {
22+
// 每頁顯示 10 篇文章
2023
pageSize: 10,
2124
});
2225
}
@@ -48,6 +51,8 @@ page.data.forEach((post) => {
4851
const posts = await getCollection("blog");
4952
const tags = new Set<string>();
5053
const categories = new Set<string>();
54+
55+
// 收集所有文章的 tags 和 categories
5156
posts.forEach((post) => {
5257
if (Array.isArray(post.data.tags)) {
5358
post.data.tags.forEach((tag) => {

src/pages/blog/[...slug].astro

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
11
---
2+
// astro: ... 引入 astro 原生的虛擬模組, slug: 通常只包含 小寫英文、數字 和 連字號 -
23
import { type CollectionEntry, getCollection, render } from "astro:content";
34
45
import BlogLayout from "../../layouts/BlogLayout.astro";
56
7+
// 動態路由的靜態路徑產生函式
68
export async function getStaticPaths() {
7-
const posts = await getCollection("blog");
9+
const posts = await getCollection("blog"); // 抓取所有文章
810
return posts.map((post, index) => ({
9-
params: { slug: post.id },
11+
params: { slug: post.id }, // slug variable = post.id
1012
props: {
13+
// 把文章資料傳給頁面使用
1114
post,
1215
prev: posts[index - 1] || null,
1316
next: posts[index + 1] || null,
1417
},
1518
}));
1619
}
20+
// 定義頁面接收的 props 類型,同時能做到 Error checking
1721
type Props = {
1822
post: CollectionEntry<"blog">;
1923
prev: CollectionEntry<"blog"> | null;
2024
next: CollectionEntry<"blog"> | null;
2125
};
2226
23-
const { post, prev, next } = Astro.props;
27+
const { post, prev, next } = Astro.props; // 藉由 Astro 框架來傳遞 props
28+
/* Equals to: (所以 Astro.props 就是一個 JavaScript 物件)
29+
const post = Astro.props.post;
30+
const prev = Astro.props.prev;
31+
const next = Astro.props.next;
32+
*/
2433
const { Content, headings } = await render(post);
2534
---
2635

@@ -38,10 +47,15 @@ const { Content, headings } = await render(post);
3847
import PhotoSwipeLightbox from "photoswipe/lightbox";
3948
import "photoswipe/style.css";
4049

50+
// 啟用了 View Transitions ,當使用者按上一頁或連結時,不會整頁刷新。
51+
// 所以使用 astro:page-load 確定每次頁面內容更新後,
52+
// 程式碼都會被執行一次,來重新綁定圖片點擊事件。
4153
document.addEventListener("astro:page-load", () => {
4254
document
4355
.querySelectorAll(".article-entry img")
4456
// @ts-expect-error - change from Element to HTMLImageElement
57+
// PhotoSwipe 套件要求圖片必須被包在 <a> 連結標籤內才能運作
58+
// 所以這裡的邏輯是把每一張圖片都包在 <a> 標籤內,並且把圖片的 src 設為連結的 href
4559
.forEach((element: HTMLImageElement) => {
4660
if (
4761
element.parentElement?.tagName === "A" ||
@@ -50,12 +64,13 @@ const { Content, headings } = await render(post);
5064
return;
5165
const a = document.createElement("a");
5266
a.href ? (a.href = element.src) : a.setAttribute("href", element.src);
67+
// 確認圖片是否載入完成,因為 PhotoSwipe 需要知道圖片的寬高才能正常運作
5368
if (element.naturalWidth || element.naturalHeight) {
5469
a.dataset.pswpWidth = element.naturalWidth + "";
5570
a.dataset.pswpHeight = element.naturalHeight + "";
5671
} else {
5772
console.warn(
58-
"Image naturalWidth and naturalHeight cannot be obtained right now, fallback to onload."
73+
"Image naturalWidth and naturalHeight cannot be obtained right now, fallback to onload.",
5974
);
6075
element.onload = () => {
6176
a.dataset.pswpWidth = element.naturalWidth + "";
@@ -73,10 +88,12 @@ const { Content, headings } = await render(post);
7388
gallery: ".article-entry",
7489
children: "a.article-gallery-item",
7590
pswpModule: () => import("photoswipe"),
91+
//// pswpModule 動態引入模組 (Code Splitting),節省效能
7692
}).init();
7793
});
7894
</script>
7995

96+
{/* 針對流程圖,目的是解決「深色模式切換」與「頁面跳轉」的問題 */}
8097
<script>
8198
import mermaid from "mermaid";
8299

@@ -130,6 +147,7 @@ const { Content, headings } = await render(post);
130147
querySelector: "pre.mermaid",
131148
});
132149
};
150+
// 當使用者切換主題時,會觸發 theme-set 事件,邏輯是重新載入 Mermaid 圖表
133151
const themeSetHandler = (e: any) => {
134152
let theme = e.detail.theme;
135153
if (e.detail.theme === "light") {
@@ -149,6 +167,7 @@ const { Content, headings } = await render(post);
149167
.catch(console.error);
150168
};
151169
const mermaidInit = () => {
170+
// 為了確保事件不會被重複綁定,先移除再添加
152171
document.body.removeEventListener("theme-set", themeSetHandler);
153172
document.body.addEventListener("theme-set", themeSetHandler);
154173

0 commit comments

Comments
 (0)