B7 review-response batch: derived about counter, CJK-aware reading time, ZH copy fixes, hero leads with capability
Deploy / build (push) Successful in 26s
Deploy / build (push) Successful in 26s
This commit is contained in:
1 parent
0b23312948
commit
5941e858cc
10 files changed
+87
-27
No files matched your search
@@ -82,6 +82,17 @@ Living list of what's done and what's next for blog.hoelee.com. Work through the
|
||||
- **Desktop banner height** capped (720px, centred): 67% → **45%** of a 900px viewport, art intact rather than cropped.
|
||||
- Measured post page: images **277 KB → 38 KB**, height **5,758 → 5,065px** desktop / 9,423 → 8,807px mobile. Script: `scripts/optimize-images.py`; format policy: `design-guide.md` §3; runbook step in `ops-runbook.md`.
|
||||
|
||||
- ✅ **B7 review-response batch, 2026-10-19** — an outside content/UX review of the live site was reconciled claim-by-claim against the repo; only the measured defects were acted on.
|
||||
- **About counter was hand-typed and had drifted**: `about.astro` said **71** write-ups while the homepage rendered **72**. Both About pages now count from the collection (`getCollection('posts', isEn|isZh).length`), so the number cannot drift again.
|
||||
- **Reading time was English-only maths**: `minutesOf` split the body on whitespace and divided by 200, so a Chinese post (no interior spaces) showed **1 min** against **5 min** for its English twin. New `src/lib/reading-time.ts` counts CJK ideographs at ~400/min plus Latin words at 200/min; pure-ASCII bodies are unchanged (same formula), the ZH post now reads **5 分钟**, and the ZH article header no longer says "min read".
|
||||
- **ZH copy defects**: `主陆开发者` → `中国大陆开发者`; `基元律动值得麻烦的原因` → `为什么值得考虑基元律动` (the English twin already said *why it is worth the hassle*).
|
||||
- **`hello-world` contradicted the site**: it still declared English primary with selective Chinese, and quoted a `~140-container homelab`. Both language twins now say every post exists in both languages and quote **162 containers across three self-managed hosts**.
|
||||
- **`0 hosting companies between me and the servers` was loose** (the VPS is rented from a provider) → `0 managed hosting layers — I run all three hosts myself` (EN + ZH).
|
||||
- **Hero sub-paragraph now leads with the capability**, not the container count; the number follows as evidence. `<h1>` and `title`/JSON-LD were left alone (A2 identity alignment).
|
||||
- **Featured bento cards got the `srcset`/`sizes` they were missing** (`-400`/`-800` webp) — they were downloading the 800px file for a 226–341px slot. At DPR1 all widths now pick `-400`; card geometry measured unchanged (226/227/310/341 px).
|
||||
- **Measured, not assumed**: 12 routes × 4 widths (360/390/414/768) = **48/48 with zero horizontal overflow** (`scrollWidth − clientWidth == 0`); art uniformity re-surveyed — 72/72 banners 1600×900 WebP, 72/72 `-800` thumbs 800×420, 72/72 `-400` thumbs 400×210, 144/144 share cards 1200×630 JPEG. Build **251 pages**, unchanged.
|
||||
- Deliberately **not** done, because the review's premise was stale or wrong: `hreflang`/canonical/`og:locale` and per-post OG images already ship (72/72 + 72/72), and the art system was already unified by B6b (round 7) — the reviewer was reading screenshots from before that rollout for the *visual* half.
|
||||
|
||||
---
|
||||
|
||||
## 2. Execution Plan (work top → bottom, one step at a time)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 等模型,统一走标准
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -47,9 +47,10 @@ const featuredMetrics: Record<string, string> = {
|
||||
<h1>{SITE.author} — full-stack developer & 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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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、流程自动化、统计和这个博客,全靠手工拴在一起。
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user