feat: locale routing (en flat, zh subfolder), language switcher, dark default
Deploy / build (push) Successful in 11s
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:
@@ -1,17 +0,0 @@
|
||||
---
|
||||
title: "你好,世界 — 关于这个博客"
|
||||
description: "这个博客的中文部分:技术文章、自托管经验、以及我在构建与学习中的记录。"
|
||||
pubDate: 2026-09-06
|
||||
category: notes
|
||||
tags: [intro]
|
||||
lang: zh
|
||||
---
|
||||
|
||||
这是博客的中文部分。我会在这里用中文分享技术文章和自托管的经验。
|
||||
|
||||
英文是主要语言,中文内容会选择性发布——通常是那些对中文读者更有价值的主题。
|
||||
|
||||
## 关于我
|
||||
|
||||
我是一名全栈开发者兼 DevOps 工程师,base 在马来西亚。我构建 Web 应用、
|
||||
自托管基础设施(约 140 个容器),并运营一个面向马来西亚中小企业的邮箱托管业务。
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: "Hello, world — about this blog"
|
||||
description: "What this blog is for: technical writing, self-hosting experience, and a record of what I build and learn."
|
||||
pubDate: 2026-09-06
|
||||
category: notes
|
||||
tags: [intro]
|
||||
---
|
||||
|
||||
Welcome. This is where I write about what I build and learn — mostly
|
||||
engineering, DevOps, and self-hosting, with some AI automation and Web3
|
||||
experiments mixed in.
|
||||
|
||||
## What to expect
|
||||
|
||||
English is the primary language here. I'll write selectively in Chinese
|
||||
(简体中文) for topics where a Chinese-speaking audience is better served —
|
||||
those posts will live under `/posts/zh/`.
|
||||
|
||||
Most posts will be one of a few shapes:
|
||||
|
||||
- **Case studies** — "how I built X", with the architecture and the mistakes.
|
||||
- **Tutorials** — a hard problem, what I tried, and the fix.
|
||||
- **Gotchas & notes** — short entries on the small things that cost me a day.
|
||||
|
||||
## About me
|
||||
|
||||
I'm Lee Teong Hoe (Mr Hoelee), a full-stack developer and DevOps engineer
|
||||
based in Malaysia. I build web applications, self-host a ~140-container
|
||||
homelab, and run an email-hosting business for Malaysian SMEs.
|
||||
|
||||
If you're a recruiter, a client, or a fellow builder — the posts here are my
|
||||
living portfolio. Start with the [latest posts](/posts/).
|
||||
@@ -4,16 +4,17 @@ description: "A walkthrough of the end-to-end pipeline that builds and serves th
|
||||
pubDate: 2026-09-06
|
||||
category: case-studies
|
||||
tags: [astro, gitea, ci-cd, self-hosting, docker, cloudflare]
|
||||
lang: en
|
||||
---
|
||||
|
||||
This blog is itself a project I built to demonstrate the kind of work I do.
|
||||
Here's the full pipeline, so the architecture is transparent.
|
||||
Here's the full pipeline, so the architecture is transparent. The source is
|
||||
public at [git.hoelee.com/hoelee/hoelee-blog](https://git.hoelee.com/hoelee/hoelee-blog)
|
||||
— read it alongside this post.
|
||||
|
||||
## The stack
|
||||
|
||||
- **Astro 5** — static site generated from Markdown.
|
||||
- **Git as the CMS** — every post is a `.md` file with YAML frontmatter, versioned in [git.hoelee.com](https://git.hoelee.com/hoelee/hoelee-blog).
|
||||
- **Git as the CMS** — every post is a `.md` file with YAML frontmatter, versioned in Gitea.
|
||||
- **Gitea Actions** — a self-hosted CI runner (`act_runner`) on my unRaid server builds the site on every push to `main`.
|
||||
- **nginx** — a dedicated container serves the static `dist/` output.
|
||||
- **Cloudflare** — the tunnel exposes it publicly, and the CDN caches everything.
|
||||
@@ -37,8 +38,102 @@ git push → Gitea webhook → act_runner picks up the job
|
||||
The whole thing runs on hardware in my homelab, which is exactly the point —
|
||||
this is a live demo of the DevOps work I describe elsewhere.
|
||||
|
||||
## The docker-compose setup
|
||||
|
||||
Here's the shape of the stack. This is the local/origin side (Gitea + the
|
||||
runner + nginx); Cloudflare handles public exposure separately via a tunnel,
|
||||
so nothing here needs a public IP or open ports.
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml — Gitea + act_runner + nginx (blog docroot)
|
||||
services:
|
||||
gitea:
|
||||
image: gitea/gitea:1.27
|
||||
container_name: gitea
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
volumes:
|
||||
- ./gitea-data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "2222:22" # SSH (optional — I use HTTPS)
|
||||
restart: unless-stopped
|
||||
|
||||
act_runner:
|
||||
image: gitea/act_runner:0.2.13
|
||||
container_name: act_runner
|
||||
environment:
|
||||
- GITEA_INSTANCE_URL=http://gitea:3000
|
||||
- GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock # runner spawns build jobs
|
||||
- ./runner-data:/data
|
||||
depends_on:
|
||||
- gitea
|
||||
restart: unless-stopped
|
||||
|
||||
nginx-blog:
|
||||
image: nginx:alpine
|
||||
container_name: nginx-blog
|
||||
volumes:
|
||||
- ./html:/usr/share/nginx/html:ro # astro build output lands here
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
The CI job (`deploy.yml`) builds `dist/` on the runner, then `cp -r dist/*`
|
||||
into the `./html` volume shared with the nginx container. Cloudflare's tunnel
|
||||
daemon (`cloudflared`) points at `nginx-blog` — that's the only thing exposed
|
||||
to the internet.
|
||||
|
||||
Two things worth calling out that cost me time:
|
||||
|
||||
1. **`act_runner` needs the Docker socket** (`/var/run/docker.sock`) mounted to
|
||||
spawn build jobs — without it, jobs sit queued forever.
|
||||
2. **The runner's `GITEA_RUNNER_REGISTRATION_TOKEN`** is a Gitea *Actions
|
||||
secret* (`${{ secrets.PAT }}` in the workflow), never a hardcoded value in
|
||||
the repo — so the repo stays safe to make public.
|
||||
|
||||
## The installation pain (real, from my commit history)
|
||||
|
||||
Getting this pipeline working was *not* smooth — the git history of this repo
|
||||
is a log of the gotchas. The four that cost real time:
|
||||
|
||||
1. **No `actions/checkout` on Gitea.** Gitea's `act_runner` does not ship
|
||||
GitHub's marketplace actions. My first workflow failed immediately — there
|
||||
was nothing to check out the repo. Fix: drop the `actions/checkout` step
|
||||
and clone manually:
|
||||
```yaml
|
||||
- name: Checkout
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
git clone --depth 1 "https://hoelee:${{ secrets.PAT }}@git.hoelee.com/hoelee/hoelee-blog.git" .
|
||||
```
|
||||
|
||||
2. **`GITHUB_TOKEN` injection doesn't authenticate the clone.** The default
|
||||
runner token wasn't enough to `git clone` a private repo, so jobs failed on
|
||||
checkout. Fix: create a dedicated **Personal Access Token** and reference it
|
||||
as `${{ secrets.PAT }}` — never hardcode it in the workflow (that's also why
|
||||
this repo is safe to make public).
|
||||
|
||||
3. **Gitea ↔ `act_runner` version mismatch.** I burned three commits
|
||||
("test gitea 1.24.7 + act_runner 0.2.13" → "gitea 1.25.5" → "gitea 1.27.3 +
|
||||
runner 3.3.2") before the runner would register and pick up jobs. The
|
||||
lesson: **match the runner to the Gitea major version** — a runner one major
|
||||
version behind a newer Gitea silently fails to register.
|
||||
|
||||
4. **`config_file` env for the runner.** The runner needed its config path
|
||||
passed explicitly before it would connect to the right instance.
|
||||
|
||||
Each of these was a "one-line fix after an hour of head-scratching" — which is
|
||||
exactly the kind of thing a blog post should save the next person from.
|
||||
|
||||
## Coming up
|
||||
|
||||
Future posts will cover the individual pieces in depth: the `act_runner`
|
||||
container setup, the Cloudflare cache rules, and the i18n routing for the
|
||||
Chinese side of this site.
|
||||
I'll write dedicated posts on the pieces that had the most hidden gotchas: the
|
||||
`act_runner` registration flow (versions + token), the Cloudflare tunnel +
|
||||
cache rules, and the locale routing (English flat, Chinese under `posts/zh/`)
|
||||
that this site now uses.
|
||||
|
||||
Reference in New Issue
Block a user