B7 review-response batch: derived about counter, CJK-aware reading time, ZH copy fixes, hero leads with capability
Deploy / build (push) Successful in 26s

This commit is contained in:
hoelee committed 2026-10-19 14:15:59 +08:00
1 parent 0b23312948
commit 5941e858cc
10 files changed
+87 -27

No files matched your search

+5 -5
View File
@@ -15,9 +15,9 @@ 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/`.
Every post here exists in two languages. I write the English version first,
and the Chinese (简体中文) version is a full translation rather than a summary —
the same slug, served under `/posts/zh/`.
Most posts will be one of a few shapes:
@@ -35,8 +35,8 @@ Most posts will be one of a few shapes:
## 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 Hoelee Enterprise — a website design & development business
based in Malaysia. I build web applications, run 162 containers
across three self-managed hosts, and run Hoelee Enterprise — a website design & development business
for Malaysian SMEs (with email hosting as a side offering, not the focus).
If you're a recruiter, a client, or a fellow builder — the posts here are my
+2 -3
View File
@@ -14,8 +14,7 @@ banner: /banners/hello-world.jpg
## 这里会有什么
英语是这里的主要语言。对于中文读者能得到更好服务的主题,我会选择性地用
简体中文写作——这些文章会放在 `/posts/zh/` 下面。
这里的每篇文章都有两种语言。英文版先写,中文版是完整翻译,而不是摘要——同一个 slug,放在 `/posts/zh/` 下面。
大部分文章会是几种形态之一:
@@ -33,7 +32,7 @@ banner: /banners/hello-world.jpg
## 关于我
我是 Lee Teong Hoe(Mr Hoelee),一名常驻马来西亚的全栈开发者兼 DevOps 工程师。
我做 Web 应用、自托管一个约 140 个容器的家用实验室,并经营 Hoelee Enterprise
我做 Web 应用,运维三台自管主机上的 162 个容器,并经营 Hoelee Enterprise
——一家面向马来西亚中小企业的网站设计与开发公司(邮箱托管是附带的服务,
不是主业)。
@@ -24,7 +24,7 @@ V4.1 Flash 大约是**每百万输入 tokens 0.15–0.30 美元、每百万输
0.60–1.20 美元**(非高峰时段半价;缓存命中的输入便宜约 20 倍)。充 10 美元
能用很久——只要合理路由,agent 工作负载能跑数千万 tokens。
问题在于:这些平台大多是为主陆开发者设计的。这意味着中国的支付渠道,
问题在于:这些平台大多是面向中国大陆开发者设计的。这意味着中国的支付渠道,
而且经常要求大陆手机号接收短信验证码。
## 第一道墙——用人民币付款
@@ -44,7 +44,7 @@ V4.1 Flash 大约是**每百万输入 tokens 0.15–0.30 美元、每百万输
如果你没有大陆号码,DeepSeek(邮箱注册)或 BytePlus ModelArk(新加坡、
美元结算)可以完全跳过这道墙——下面会详述。
基元律动值得麻烦的原因:
为什么值得考虑基元律动:
- **一个 API Key,OpenAI 兼容。** 聚合了 GLM-5.3、Qwen3.8-Max、DeepSeek V4、
Kimi K2.7-Code、MiniMax、Seed 等模型,统一走标准
+35
View File
@@ -0,0 +1,35 @@
/**
* Reading time and word count that also work for Chinese.
*
* A whitespace split is meaningless for CJK prose: an entire Chinese article has
* almost no interior spaces, so it counted as one "word" and every ZH post showed
* "1 min read" while its English twin showed five. CJK ideographs are counted as
* characters (~400/min); everything else keeps the existing ~200 words/min.
*
* For pure-ASCII bodies the result is identical to the old whitespace/200 maths.
*/
const CJK_CHAR = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
function counts(body?: string): { cjk: number; latin: number } {
const text = body ?? '';
const cjk = (text.match(new RegExp(CJK_CHAR.source, 'g')) ?? []).length;
const latin = text
.replace(new RegExp(CJK_CHAR.source, 'g'), ' ')
.trim()
.split(/\s+/)
.filter(Boolean).length;
return { cjk, latin };
}
/** Total count used for schema.org `wordCount` (CJK counts per character). */
export function countWords(body?: string): number {
const { cjk, latin } = counts(body);
return cjk + latin;
}
/** Minutes of reading time, never below 1. */
export function readingMinutes(body?: string): number {
const { cjk, latin } = counts(body);
return Math.max(1, Math.round(cjk / 400 + latin / 200));
}
+8 -2
View File
@@ -1,7 +1,13 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import AuthorCard from '../components/AuthorCard.astro';
import { getCollection } from 'astro:content';
import { isEn } from '../lib/lang';
import { SITE } from '../config';
// Counted from the content collection, never typed by hand — a hardcoded number
// here already drifted once (71 while the homepage rendered 72).
const writeUps = (await getCollection('posts', isEn)).length;
---
<BaseLayout
@@ -47,8 +53,8 @@ import { SITE } from '../config';
<div class="proof-grid">
<div class="proof-card"><span class="n">162</span><span class="l">containers in production</span></div>
<div class="proof-card"><span class="n">3</span><span class="l">self-managed hosts (78 compose stacks)</span></div>
<div class="proof-card"><span class="n">71</span><span class="l">write-ups, each in English and 中文</span></div>
<div class="proof-card"><span class="n">0</span><span class="l">hosting companies between me and the servers</span></div>
<div class="proof-card"><span class="n">{writeUps}</span><span class="l">write-ups, each in English and 中文</span></div>
<div class="proof-card"><span class="n">0</span><span class="l">managed hosting layers — I run all three hosts myself</span></div>
</div>
<p>
That footprint is client websites, a mail platform, CI/CD, workflow automation,
+6 -3
View File
@@ -47,9 +47,10 @@ const featuredMetrics: Record<string, string> = {
<h1>{SITE.author} — full-stack developer &amp; DevOps engineer</h1>
</div>
<p class="hero-sub">
I run <strong>162 containers across three self-managed hosts</strong> — client sites, a
mail platform, CI/CD, workflow automation and this blog — and I build web systems for
Malaysian SMEs. I write up what broke, what the evidence said, and how I fixed it.
I build and operate production web systems — from the code down to the servers.
<strong>162 containers across three self-managed hosts</strong> run client sites, a mail
platform, CI/CD, workflow automation and this blog. I write up what broke, what the
evidence said, and how I fixed it.
</p>
<p class="hero-cta">
<a class="btn btn-primary" href="#featured">Read the engineering case studies ↓</a>
@@ -72,6 +73,8 @@ const featuredMetrics: Record<string, string> = {
<div class="bento-visual" aria-hidden="true">
<img
src={`/og/thumb/${cleanSlug}-800.webp`}
srcset={`/og/thumb/${cleanSlug}-400.webp 400w, /og/thumb/${cleanSlug}-800.webp 800w`}
sizes="(max-width: 640px) 60vw, 341px"
alt=""
loading="lazy"
decoding="async"
+7 -7
View File
@@ -3,6 +3,7 @@ import BaseLayout from '../../layouts/BaseLayout.astro';
import AuthorCard from '../../components/AuthorCard.astro';
import { getCollection, render } from 'astro:content';
import { postLang } from '../../lib/lang';
import { countWords, readingMinutes } from '../../lib/reading-time';
import { tagCounts, tagLink } from '../../lib/tags';
import { SITE } from '../../config';
@@ -19,15 +20,14 @@ 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));
// Reading time — CJK-aware (src/lib/reading-time.ts). A plain whitespace split
// made every Chinese post read "1 min" while its English twin read five.
const words = countWords(post.body);
const minutes = readingMinutes(post.body);
const fmt = (d: Date) =>
d.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', { year: 'numeric', month: 'long', day: 'numeric' });
const minutesOf = (body?: string) => Math.max(1, Math.round((body?.trim().split(/\s+/).length ?? 0) / 200));
// Related posts: same language, same category or overlapping tags, excluding self
const all = await getCollection('posts', ({ data }) => !data.draft);
// Tag pills: a tag with its own page becomes a link, a one-off tag stays plain text.
@@ -97,7 +97,7 @@ const jsonLd = {
<div class="meta">
<time datetime={post.data.pubDate.toISOString()}>{fmt(post.data.pubDate)}</time>
<span class="sep">·</span>
<span>{minutes} min read</span>
<span>{minutes} {lang === 'zh' ? '分钟' : 'min read'}</span>
<span class="sep">·</span>
<a class="tag" href={lang === 'zh' ? `/zh/categories/${post.data.category}/` : `/categories/${post.data.category}/`}>{post.data.category}</a>
{post.data.tags.map((t) => {
@@ -135,7 +135,7 @@ const jsonLd = {
<span class="related-badge">{p.data.category}</span>
<h3>{p.data.title}</h3>
<time datetime={p.data.pubDate.toISOString()}>
{fmt(p.data.pubDate)} · {minutesOf(p.body)} {lang === 'zh' ? '分钟' : 'min read'}
{fmt(p.data.pubDate)} · {readingMinutes(p.body)} {lang === 'zh' ? '分钟' : 'min read'}
</time>
</div>
</a>
+8 -2
View File
@@ -1,7 +1,13 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import AuthorCard from '../../components/AuthorCard.astro';
import { getCollection } from 'astro:content';
import { isZh } from '../../lib/lang';
import { SITE } from '../../config';
// Counted from the content collection, never typed by hand — a hardcoded number
// here already drifted once (71 while the homepage rendered 72).
const writeUps = (await getCollection('posts', isZh)).length;
---
<BaseLayout
@@ -46,8 +52,8 @@ import { SITE } from '../../config';
<div class="proof-grid">
<div class="proof-card"><span class="n">162</span><span class="l">个生产环境容器</span></div>
<div class="proof-card"><span class="n">3</span><span class="l">台自管服务器(78 个 compose 栈)</span></div>
<div class="proof-card"><span class="n">71</span><span class="l">篇长文,每篇中英双版</span></div>
<div class="proof-card"><span class="n">0</span><span class="l">夹在我和服务器之间的托管商</span></div>
<div class="proof-card"><span class="n">{writeUps}</span><span class="l">篇长文,每篇中英双版</span></div>
<div class="proof-card"><span class="n">0</span><span class="l">层托管服务——三台主机都我自己运维</span></div>
</div>
<p>
这套环境里有客户网站、邮件平台、CI/CD、流程自动化、统计和这个博客,全靠手工拴在一起。
+3 -3
View File
@@ -51,9 +51,7 @@ const featuredMetrics: Record<string, string> = {
<h1>{SITE.author} —— 全栈开发者兼 DevOps 工程师</h1>
</div>
<p class="hero-sub">
我独自运维 <strong>三台自管服务器上的 162 个容器</strong>(客户站点、邮件平台、
CI/CD、流程自动化与这个博客),并为马来西亚中小企业构建 Web 系统。这里写的是
「坏了什么、证据在哪、怎么修好」。
从代码到服务器,生产环境的 Web 系统我自己从头做到尾。<strong>三台自管服务器上跑着 162 个容器</strong>:客户站点、邮件平台、CI/CD、流程自动化,还有这个博客。这里写的是「坏了什么、证据在哪、怎么修好」。
</p>
<p class="hero-cta">
<a class="btn btn-primary" href="#featured">先看工程案例 ↓</a>
@@ -76,6 +74,8 @@ const featuredMetrics: Record<string, string> = {
<div class="bento-visual" aria-hidden="true">
<img
src={`/og/thumb/${cleanSlug}-800.webp`}
srcset={`/og/thumb/${cleanSlug}-400.webp 400w, /og/thumb/${cleanSlug}-800.webp 800w`}
sizes="(max-width: 640px) 60vw, 341px"
alt=""
loading="lazy"
decoding="async"