feat: locale routing (en flat, zh subfolder), language switcher, dark default
Deploy / build (push) Successful in 11s

- Derive lang from folder (posts/zh/* -> zh, else en); remove lang: frontmatter
- Change post route to [...slug] to capture nested zh paths
- Add nav language switcher (EN <-> 中文) + /zh/ landing page
- Default theme to dark (light opt-in via toggle)
- Translate hello-world-zh placeholder into English intro post
- Update case study: docker-compose sample + real install gotchas (commit history)
- Add docs/project-state.md (Tier 1/2/3 backlog)
This commit is contained in:
hoelee committed 2026-09-06 06:37:34 +08:00
1 parent a0b238bc98
commit e0d4b0fbb7
17 files changed
+318 -44

No files matched your search

+3 -2
View File
@@ -2,16 +2,17 @@
import BaseLayout from '../../layouts/BaseLayout.astro';
import PostList from '../../components/PostList.astro';
import { getCollection } from 'astro:content';
import { isEn } from '../../lib/lang';
export async function getStaticPaths() {
const posts = await getCollection('posts', ({ data }) => !data.draft && data.lang === 'en');
const posts = await getCollection('posts', isEn);
const cats = [...new Set(posts.map((p) => p.data.category))];
return cats.map((cat) => ({ params: { category: cat } }));
}
const { category } = Astro.params;
const posts = (await getCollection('posts', ({ data }) => !data.draft && data.lang === 'en'))
const posts = (await getCollection('posts', isEn))
.filter((p) => p.data.category === category)
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
+2 -1
View File
@@ -1,8 +1,9 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
import { isEn } from '../../lib/lang';
const posts = await getCollection('posts', ({ data }) => !data.draft && data.lang === 'en');
const posts = await getCollection('posts', isEn);
const categories = [...new Set(posts.map((p) => p.data.category))].sort();
+2 -1
View File
@@ -2,9 +2,10 @@
import BaseLayout from '../layouts/BaseLayout.astro';
import PostList from '../components/PostList.astro';
import { getCollection } from 'astro:content';
import { isEn } from '../lib/lang';
import { SITE } from '../config';
const posts = (await getCollection('posts', ({ data }) => !data.draft && data.lang === 'en'))
const posts = (await getCollection('posts', isEn))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
@@ -2,6 +2,7 @@
import BaseLayout from '../../layouts/BaseLayout.astro';
import AuthorCard from '../../components/AuthorCard.astro';
import { getCollection, render } from 'astro:content';
import { postLang } from '../../lib/lang';
export async function getStaticPaths() {
const posts = await getCollection('posts', ({ data }) => !data.draft);
@@ -14,17 +15,19 @@ export async function getStaticPaths() {
const { post } = Astro.props;
const { Content } = await render(post);
const lang = postLang(post.slug);
// Reading time (~200 wpm)
const words = post.body?.trim().split(/\s+/).length ?? 0;
const minutes = Math.max(1, Math.round(words / 200));
const fmt = (d: Date) =>
d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
d.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'long', day: 'numeric' });
// Related posts: same category or overlapping tags, excluding self
// Related posts: same language, same category or overlapping tags, excluding self
const all = await getCollection('posts', ({ data }) => !data.draft);
const related = all
.filter((p) => p.slug !== post.slug && p.data.lang === post.data.lang)
.filter((p) => p.slug !== post.slug && postLang(p.slug) === lang)
.map((p) => {
const catMatch = p.data.category === post.data.category ? 2 : 0;
const tagOverlap = p.data.tags.filter((t) => post.data.tags.includes(t)).length;
@@ -39,7 +42,7 @@ const related = all
<BaseLayout
title={`${post.data.title} — Mr Hoelee`}
description={post.data.description}
lang={post.data.lang}
lang={lang}
ogImage={post.data.ogImage ?? '/og-default.png'}
type="article"
article={{
@@ -69,7 +72,7 @@ const related = all
{related.length > 0 && (
<section class="related">
<h2>Related posts</h2>
<h2>{lang === 'zh' ? '相关文章' : 'Related posts'}</h2>
<div class="related-grid">
{related.map((p) => (
<a class="related-card" href={`/posts/${p.slug}/`}>
+2 -1
View File
@@ -2,8 +2,9 @@
import BaseLayout from '../../layouts/BaseLayout.astro';
import PostList from '../../components/PostList.astro';
import { getCollection } from 'astro:content';
import { isEn } from '../../lib/lang';
const posts = (await getCollection('posts', ({ data }) => !data.draft && data.lang === 'en'))
const posts = (await getCollection('posts', isEn))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
+2 -1
View File
@@ -1,9 +1,10 @@
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
import { isEn } from '../lib/lang';
import { SITE } from '../config';
export async function GET(context) {
const posts = (await getCollection('posts', ({ data }) => !data.draft && data.lang === 'en'))
const posts = (await getCollection('posts', isEn))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return rss({
+39
View File
@@ -0,0 +1,39 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import PostList from '../../components/PostList.astro';
import { getCollection } from 'astro:content';
import { isZh } from '../../lib/lang';
import { SITE } from '../../config';
const posts = (await getCollection('posts', isZh))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
<BaseLayout
title={`${SITE.handle} — 技术博客`}
description="Hoelee 的技术博客中文版:工程、DevOps、自托管、AI 自动化与 Web3 实验。"
lang="zh"
>
<section>
<h1>你好,我是 {SITE.author}。</h1>
<p>
一名base在马来西亚的全栈开发者兼 DevOps 工程师。我构建 Web 应用,
自托管一个约 140 个容器的 homelab,并运营面向马来西亚中小企业的
邮箱托管业务。这里是博客的中文部分。
</p>
<p>
<a href="/">切换到英文</a>
<span class="sep">·</span>
<a href="/about/">关于我</a>
</p>
</section>
<section>
<h2>最新文章</h2>
{posts.length === 0 ? (
<p><em>中文文章即将发布,敬请期待。</em></p>
) : (
<PostList posts={posts} />
)}
</section>
</BaseLayout>