feat(content): implement consultant review recommendations (executive summary, dynamic cta, homelab cleanup)
Deploy / build (push) Successful in 25s
Deploy / build (push) Successful in 25s
This commit is contained in:
1 parent
5ecf25d37c
commit
355fecd348
28 files changed
+1634
-49
No files matched your search
@@ -0,0 +1,388 @@
|
||||
这份由 Hermes Agent 整理的补充资料包非常详实,代码结构清晰,尤其是对第四部分“主站断层与 GSC 零收录原因”的诊断非常诚实且精准。
|
||||
|
||||
在直接进入你要求的 **4 项重点开刀示范** 之前,我先针对你刚暴露出来的 **3 个工程与内容层面的紧急漏洞** 给出快刀斩乱麻的修复指令(这部分一天之内就能改完,但收益巨大):
|
||||
|
||||
### 🚨 紧急修复指令(Day-1 Quick Wins)
|
||||
|
||||
1. **主站 `[www.hoelee.com](https://www.hoelee.com)` 到 `blog.hoelee.com` 的权重导流(解决孤岛与冷启动)**:
|
||||
|
||||
|
||||
* 既然主站是 WordPress 且已有一定历史权重,却对博客“0 入链”,请立即在主站 `[www.hoelee.com](https://www.hoelee.com)` 的顶部主导航栏(Main Menu)**和**页脚(Footer)加上 `Engineering Blog` 或 `Case Studies` 链接指向 `[https://blog.hoelee.com/](https://blog.hoelee.com/)`。
|
||||
|
||||
|
||||
* 立即清理主站那 400 个旧版媒体附件薄弱页(Thin attachment pages)(在 WordPress SEO 插件如 RankMath / Yoast 中开启“Redirect attachment URLs to parent post/file”,否则会严重拖累整个 `hoelee.com` 根域名的抓取预算)。
|
||||
|
||||
|
||||
* 立即登录 GSC,在 `hoelee.com` 域名属性下提交 `[https://blog.hoelee.com/sitemap-index.xml](https://blog.hoelee.com/sitemap-index.xml)`,并用 URL Inspection 工具手动请求抓取首页和 4 篇 Featured Case Studies。
|
||||
|
||||
|
||||
|
||||
|
||||
2. **修复样本 1 中的“规范违背”(Persona 破功)**:
|
||||
* 在你第一轮发我的规范中,明确写道:**“严格禁用 `my home lab` 等爱好者口吻”**。但在样本 1《DigiKedai Bot》的 `## The architecture` 第 2 段第一句,赫然写着:*“My **homelab** is behind CGNAT...”*。
|
||||
|
||||
|
||||
* **行动**:让 Hermes Agent 对全站 91 篇英文和 91 篇中文 Markdown 跑一次全局正则扫描(搜索 `homelab`、`home lab`、`家用`、`小主机`),统一替换为 `self-hosted production node` / `on-premise host` / `自托管生产节点`。
|
||||
|
||||
|
||||
|
||||
|
||||
3. **打通 Markdown 到 Astro 组件的渲染能力**:
|
||||
* 目前你的文章全是 `.md` 格式。为了不破坏现有的 182 篇 `.md` 文件的兼容性,**千万不要把所有文件改成 `.mdx**`。最优雅的工程方案是:将 `<ExecutiveSummary/>` 和 `<DynamicCTA/>` 的结构化数据写入文章的 **Frontmatter(YAML)**,由 `src/pages/posts/[...slug].astro` 统一读取并在 `<Content/>` 的一头一尾自动渲染;而正文里的折叠代码区,直接使用原生 HTML5 的 `<details><summary>` 标签(Markdown 原生兼容,只需在 `global.css` 里写好极客风样式)。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
下面,我针对你清单中的 **4 大核心任务**,给出可直接复制进项目的代码级示范与重构方案:
|
||||
|
||||
---
|
||||
|
||||
## 示范一:首屏速览卡片(`<ExecutiveSummary/>`)实例文案与实现
|
||||
|
||||
|
||||
|
||||
### 1. Frontmatter 结构升级与双套实例文案
|
||||
|
||||
在 `src/content/config.ts` 的 schema 中增加可选的 `summary` 字段。针对两类完全不同的受众(样本 1 面向中小企业老板/产品负责人,样本 2 面向 CTO/资深运维招聘官),文案侧重点完全不同:
|
||||
|
||||
#### 📌 样本 1:《How I Built the DigiKedai Telegram AI Bot》(商业交付侧重)
|
||||
|
||||
|
||||
|
||||
```yaml
|
||||
# 新增至 how-i-built-the-digikedai-telegram-bot.md 的 frontmatter
|
||||
summary:
|
||||
audience: "Business Owners & Product Leads"
|
||||
painPoint: "1人数字商品电商面临大量重复售前咨询(试用申请、账号开通、选品),夜间与周末漏单严重,人工客服成本高。"
|
||||
impact:
|
||||
- metric: "24/7 秒级响应"
|
||||
detail: "全天候接管 Telegram 与 WhatsApp 双端咨询,零人工介入自动发放免费试用账号"
|
||||
- metric: "0 价格幻觉"
|
||||
detail: "三层确定性 SKU 检索 + 状态机拦截购买意图,杜绝大模型胡报价或泄漏内部路径"
|
||||
- metric: "~175 项单测护航"
|
||||
detail: "单容器稳定运行于 NAS,通过自建 LiteLLM 网关实现模型高可用切换"
|
||||
stack: ["TypeScript", "grammY", "LiteLLM", "NocoDB", "Cloudflare Tunnel", "PostgreSQL"]
|
||||
jumpLinks:
|
||||
business: "#why-a-business-needs-a-bot-like-this"
|
||||
architecture: "#the-architecture"
|
||||
result: "#the-result"
|
||||
|
||||
```
|
||||
|
||||
#### 📌 样本 2:《Docker Reported Exit 0. The Kernel Had Already OOM-Killed It.》(硬核工程/招聘侧重)
|
||||
|
||||
|
||||
|
||||
```yaml
|
||||
# 新增至 docker-reported-exit-0-the-oom-killer-disagreed.md 的 frontmatter
|
||||
summary:
|
||||
audience: "SREs, DevOps Leads & Systems Engineers"
|
||||
painPoint: "公网边缘反向代理容器(Traefik)4天内无预警重启7次,但 Docker daemon 谎报 ExitCode=0 且 OOMKilled=false,常规监控完全失明。"
|
||||
impact:
|
||||
- metric: "精准定位真凶"
|
||||
detail: "穿透 Docker 状态假象,通过内核 dmesg 与 cgroup 内存水位线抓出 25,976 个代理爬虫 IP 引发的 512MB 触顶"
|
||||
- metric: "0 停机热修复"
|
||||
detail: "单容器精准扩容至 2GB 并锁死 Swap(防磁盘阵列雪崩),18分钟峰值回落至 210MB,连续运行 0 崩溃"
|
||||
- metric: "防自锁诊断规范"
|
||||
detail: "沉淀边缘网关高并发防爬虫压测与避免自身 IP 被 WAF 误封(403 假象)的排障 SOP"
|
||||
stack: ["Docker", "Linux Kernel (dmesg/cgroups)", "Traefik", "Synology DSM", "Security Filtering"]
|
||||
jumpLinks:
|
||||
business: "#the-result"
|
||||
architecture: "#the-kernel-keeps-its-own-record"
|
||||
result: "#what-i-would-do-differently"
|
||||
|
||||
```
|
||||
|
||||
### 2. Astro 组件代码(`src/components/ExecutiveSummary.astro`)
|
||||
|
||||
将此组件插入 `src/pages/posts/[...slug].astro` 的 `<TableOfContents/>` 之前:
|
||||
|
||||
```astro
|
||||
---
|
||||
interface Props {
|
||||
summary?: {
|
||||
audience: string;
|
||||
painPoint: string;
|
||||
impact: { metric: string; detail: string }[];
|
||||
stack: string[];
|
||||
jumpLinks?: { business?: string; architecture?: string; result?: string };
|
||||
};
|
||||
lang?: 'en' | 'zh';
|
||||
}
|
||||
const { summary, lang = 'en' } = Astro.props;
|
||||
if (!summary) return null;
|
||||
|
||||
const labels = lang === 'zh' ? {
|
||||
badge: '⚡ 核心速览 (Executive Summary)',
|
||||
for: '适用读者:',
|
||||
problem: '🎯 核心痛点',
|
||||
impact: '📈 交付成果与工程指标',
|
||||
stack: '🛠️ 涉及技术栈',
|
||||
nav: '⏱️ 快速跳读:',
|
||||
btnBiz: '看商业/最终结果 ↓',
|
||||
btnArch: '看系统架构与排障 ↓',
|
||||
btnLesson: '看复盘与避坑指南 ↓'
|
||||
} : {
|
||||
badge: '⚡ Executive Summary',
|
||||
for: 'Target Audience:',
|
||||
problem: '🎯 The Problem',
|
||||
impact: '📈 Key Outcomes & Metrics',
|
||||
stack: '🛠️ Architecture Stack',
|
||||
nav: '⏱️ Quick Jump:',
|
||||
btnBiz: 'Business Outcome ↓',
|
||||
btnArch: 'Architecture & Root Cause ↓',
|
||||
btnLesson: 'Lessons Learned ↓'
|
||||
};
|
||||
---
|
||||
<aside class="exec-summary" aria-label={labels.badge}>
|
||||
<div class="exec-header">
|
||||
<span class="exec-badge">{labels.badge}</span>
|
||||
<span class="exec-audience">{labels.for} <strong>{summary.audience}</strong></span>
|
||||
</div>
|
||||
|
||||
<div class="exec-problem">
|
||||
<strong>{labels.problem}:</strong> {summary.painPoint}
|
||||
</div>
|
||||
|
||||
<div class="exec-grid">
|
||||
{summary.impact.map((item) => (
|
||||
<div class="exec-metric-card">
|
||||
<div class="exec-metric-val">{item.metric}</div>
|
||||
<div class="exec-metric-desc">{item.detail}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="exec-footer">
|
||||
<div class="exec-stack">
|
||||
<span>{labels.stack}:</span>
|
||||
{summary.stack.map(t => <code class="exec-pill">{t}</code>)}
|
||||
</div>
|
||||
{summary.jumpLinks && (
|
||||
<div class="exec-jumps">
|
||||
<span>{labels.nav}</span>
|
||||
{summary.jumpLinks.business && <a href={summary.jumpLinks.business}>{labels.btnBiz}</a>}
|
||||
{summary.jumpLinks.architecture && <a href={summary.jumpLinks.architecture}>{labels.btnArch}</a>}
|
||||
{summary.jumpLinks.result && <a href={summary.jumpLinks.result}>{labels.btnLesson}</a>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 示范二:正文排版重构、视觉前置与代码折叠(`<TechnicalDeepDive/>`)
|
||||
|
||||
|
||||
|
||||
很多技术博客之所以留不住老板,是因为**把最枯燥的文本架构和细节堆在前面,把最性感的成果藏在最后**。以下是针对两篇样本的具体手术刀级重构方案:
|
||||
|
||||
### 🔪 针对样本 1(《DigiKedai Bot》)的重构指令
|
||||
|
||||
:
|
||||
|
||||
1. **前 30% 黄金视觉区改造(替换简陋的 ASCII 箭头图)**:
|
||||
* 原文 `## The architecture` 下方那段纯文本箭头(`Customer → Telegram/WhatsApp → ...`)显得过于简陋。
|
||||
|
||||
|
||||
* **改造**:将其替换为一张清晰的 **双轨制状态机架构图(SVG 或 Mermaid)**,重点标出两条颜色不同的路径:
|
||||
* 🟢 **确定性快车道(Deterministic Fast-Track)**:`购买意图 / /trial 命令 / Deep Link` ➔ **绕过 LLM** ➔ 直接写 NocoDB 发放账号 / 弹出内联菜单。
|
||||
|
||||
|
||||
* 🔵 **语义检索慢车道(RAG LLM Path)**:`开放式商品提问` ➔ 三层 SKU 检索器(自动捆绑免费试用版 `FREECZH01`) ➔ LiteLLM (`mem0-openai`) ➔ 精准回复。
|
||||
|
||||
|
||||
|
||||
|
||||
* **新增 15 秒实机演示视频**:在 `## Why a business needs a bot like this` 下方,立即嵌入一段左右分屏的 15 秒无声循环 WebM 动图(左边是 Telegram 客户端发一句 *“我要这个试用”*,右边是 NocoDB 表格秒级自动多出一行记录并回复账号密码)。这能让中小企业老板在 **第 15 秒** 就产生付费冲动。
|
||||
|
||||
|
||||
|
||||
|
||||
2. **应该收进折叠区(`<details class="tech-deep-dive">`)的内容**:
|
||||
* **折叠点 1**:`## Retrieval: how the bot actually knows the catalogue` 中的具体正则匹配规则(`/\b[A-Z]{2,}\d{2,}\b/gi` 及中英文分词逻辑)。
|
||||
|
||||
|
||||
* *保留在外面的人话*:“为了防止大模型胡编乱造价格,我设计了三层确定性检索器,且在命中付费课程时会自动关联其免费试用版(把原本生硬的‘没有’变成了自动推销)。”
|
||||
|
||||
|
||||
* *折叠按钮文案*:`🔍 展开查看:三层 SKU 检索算法与防幻觉 Prompt 注入细节 (For Engineers)`
|
||||
|
||||
|
||||
* **折叠点 2**:`## The bugs that ate an afternoon each` 中的 3 个具体排障过程。
|
||||
|
||||
|
||||
* *保留在外面的人话*:用 3 行 Bullet points 总结教训(1. Telegram 超时导致的死循环重发;2. LiteLLM 别名强校验;3. WhatsApp 插件空模板发送字面量 `m_text` 的坑)。
|
||||
|
||||
|
||||
* *折叠按钮文案*:`🛠️ 展开查看:Webhook 50s 超时阻断配置与 WhatsApp 载荷契约排障实录`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 🔪 针对样本 2(《Docker Exit 0 & OOM Killer》)的重构指令
|
||||
|
||||
:
|
||||
|
||||
> **注意**:样本 2 是一篇写给 Tech Lead 和同行看的顶级 `devops` 硬核排障文。对于这类文章,**千万不要过度折叠核心证据**,因为 `dmesg` 日志和表格正是吸引技术面试官的高光时刻!
|
||||
>
|
||||
>
|
||||
|
||||
1. **前 20% 视觉冲击力升级**:
|
||||
* 将 `## Docker's report says nothing happened` 和 `## The kernel keeps its own record` 的两段代码块,在桌面端改造为 **左右并排对比卡片(Side-by-Side Diff View)**:
|
||||
|
||||
|
||||
* 左侧卡片(标红 ❌ **What Docker Reported**):`ExitCode: 0`, `OOMKilled: false`
|
||||
|
||||
|
||||
* 右侧卡片(标绿 ✅ **What Kernel dmesg Recorded at the Exact Second**):`Killed process 615 (traefik) ... anon-rss:478480kB`
|
||||
|
||||
|
||||
|
||||
|
||||
* 这种视觉对比会让任何懂行的运维/架构师一眼看出文章的含金量。
|
||||
|
||||
|
||||
2. **应该收进折叠区的内容**:
|
||||
* 在 `## The fix, and the sentence that has to come with it` 下方,你列出了一长串 Verification 校验细节(挂载列表核对、插件目录检查、路由 200 状态恢复等)。这部分属于严谨但偏冗长的运维 Checklist,适合放入折叠框:
|
||||
|
||||
|
||||
* *折叠按钮文案*:`📋 展开查看:生产容器热迁移与 Cgroup 内存上限生效完整验证清单 (5-Step Verification)`
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 示范三:高转化动态 CTA 矩阵实战文案(`<DynamicCTA/>`)
|
||||
|
||||
|
||||
|
||||
扔掉原本底部千篇一律的 `Want a bot like this for your business?`。我们在 `src/components/DynamicCTA.astro` 中根据文章的 `category` 和 `tags` 自动渲染直击痛点的转化钩子,并且**同时兼顾“老板外包询盘”与“雇主/猎头招聘”双目标**:
|
||||
|
||||
### 1. 针对样本 1(`category: case-studies` / `ai`)的专属渲染效果:
|
||||
|
||||
> ### 🚀 想要一个 24/7 自动接单、发试用、不漏单的 AI 销冠?
|
||||
>
|
||||
>
|
||||
> 别让夜间和周末的客户询盘因为“没人回”而流失给竞争对手。我帮中小企业从零搭建 **Telegram / WhatsApp 智能客服与业务自动化流水线**(连接你的商品库、CRM 与自动发货系统,无高昂 SaaS 按人头月费)。
|
||||
> * 💬 **企业主定制咨询**:**[点击通过 WhatsApp 发我你的业务场景 (+60 12-797 2969)](https://www.google.com/search?q=https://wa.me/60127972969%3Ftext%3DHi%2520Hoelee,%2520I%2520read%2520your%2520DigiKedai%2520AI%2520Bot%2520case%2520study%2520and%2520want%2520to%2520discuss%2520automation%2520for%2520my%2520business.)** —— 免费为你做 15 分钟自动化可行性与成本评估。
|
||||
> * 👔 **团队招聘 / 技术合作**:正在寻找懂状态机架构、能把 LLM 真正落地到生产环境的工程师?**[查看我的工程能力全景与简历 (/about)](https://www.google.com/search?q=/about/)** 或发邮件至 [[email protected]](https://www.google.com/search?q=mailto%3Ame%40hoelee.com)。
|
||||
>
|
||||
>
|
||||
|
||||
### 2. 针对样本 2(`category: devops` / `engineering`)的专属渲染效果:
|
||||
|
||||
> ### 🛡️ 服务器莫名崩溃、云账单失控,或者缺一位能看懂内核日志的架构师?
|
||||
>
|
||||
>
|
||||
> 真正的生产环境排障,靠的不是重启服务器,而是穿透表象抓出根因。我目前独自维护着 **跨 3 台物理/云主机的 162 个生产容器与高可用网络栈**。
|
||||
>
|
||||
>
|
||||
> * 👨💻 **致招聘官与 Tech Leads**:如果你需要一位兼具 Full-Stack 开发能力与底层 Linux/Docker/网关排障功底的实战派工程师,**[点击查看我的自托管基础设施蓝图与履历 →](https://www.google.com/search?q=/about/)**
|
||||
> * 🔧 **致创业团队与企业主**:需要搭建高安全性的自托管基础设施、企业邮局(SifuMail)或做一次彻底的服务器稳定性诊断?**[直接在 WhatsApp 上联系我 ↗](https://www.google.com/search?q=https://wa.me/60127972969%3Ftext%3DHi%2520Hoelee,%2520I%2520read%2520your%2520Docker%2520OOM%2520debugging%2520post%2520and%2520need%2520help%2520with%2520our%2520infrastructure.)**
|
||||
>
|
||||
>
|
||||
|
||||
---
|
||||
|
||||
## 示范四:3 大系统蓝图(Pillar Pages)的 Astro 路由与挂载规划
|
||||
|
||||
|
||||
|
||||
### 1. 路由决策:独立顶层路由 `/blueprints/` vs. 整合进 `/categories/`?
|
||||
|
||||
|
||||
|
||||
**明确结论:必须采用独立的顶层路由 `/blueprints/`(中文 `/zh/blueprints/`),绝对不要塞进 `/categories/`!**
|
||||
|
||||
**原因有二**:
|
||||
|
||||
1. **UX 与心智层级不同**:`/categories/devops` 是一个按发布时间倒序排列的“文章归档列表(Feed)”,读者点进去看到的是零散的树叶;而 `/blueprints/production-infrastructure` 是一份**精心编排的、带全局拓扑图的“系统白皮书/架构说明书”**,读者看到的是整棵大树的主干。
|
||||
|
||||
|
||||
2. **SEO 权重漏斗(Topic Authority Hub)**:在 Google 眼里,分页的 Category 列表页属于低信息密度的导航页(Thin Hub),极难获得高排名。而一个拥有 2500 字架构综述 + 交互式拓扑图 + 20 个精准内链指向子文章的独立 Pillar Page,是典型的 **Evergreen Cornerstone Content(常青基石内容)**。
|
||||
|
||||
### 2. 具体目录结构与导航改造方案
|
||||
|
||||
#### 第一步:在 `src/pages/` 下新增蓝图路由
|
||||
|
||||
|
||||
|
||||
```text
|
||||
src/pages/
|
||||
├── blueprints/
|
||||
│ ├── index.astro # 3 大系统蓝图总览页(替代或升级原本单薄的分类导航)
|
||||
│ ├── self-hosted-infrastructure.astro # 蓝图 1:162 容器生产级自托管基础设施全景
|
||||
│ ├── ai-agent-automation.astro # 蓝图 2:确定性状态机 + 本地/网关 LLM 业务自动化架构
|
||||
│ └── sme-digital-suite.astro # 蓝图 3:中小企业建站、SifuMail 邮局与数字资产交付体系
|
||||
└── zh/blueprints/ # 镜像中文版蓝图路由
|
||||
|
||||
```
|
||||
|
||||
#### 第二步:顶部导航栏(`BaseLayout.astro`)高价值升级
|
||||
|
||||
|
||||
|
||||
目前你的导航栏是:`Posts` | `Categories` | `About` | `RSS`。这太像普通个人博客了。建议立刻改为:
|
||||
|
||||
* 英文:`Blueprints (系统蓝图)` | `Case Studies (商业案例)` | `All Posts (全部文章)` | `Work With Me (合作/雇佣)`
|
||||
* (注:把低频点击的 `RSS` 移到页脚 Footer 即可,把寸土寸金的顶部导航留给最高转化的入口!)
|
||||
|
||||
|
||||
|
||||
#### 第三步:单篇蓝图页(如 `/blueprints/self-hosted-infrastructure`)的黄金版面结构
|
||||
|
||||
当你让 Hermes Agent 生成这 3 个页面时,遵循以下 4 段式模板:
|
||||
|
||||
1. **首屏全局拓扑图**:直接嵌入你在首页用到的架构全景图(可点击放大或分模块高亮),列出硬核指标(3 Hosts: Unraid + Synology + VPS, 162 Containers, 78 Compose Stacks)。
|
||||
|
||||
|
||||
2. **核心架构设计哲学(300~500 字)**:例如讲清楚为什么你用 Cloudflare Tunnel + Traefik + Authentik 做双层零信任入口,为什么备份采用 3-2-1 异地自愈策略。
|
||||
3. **按架构层级挂载你的 91 篇实战文章(Cluster Matrix)**:
|
||||
|
||||
|
||||
* 不要按时间排!按架构层级排:
|
||||
* **Layer 1: 边缘网关与安全认证 (Ingress & Auth)** ➔ 挂载《The Forward-Auth Gate That Verified Perfectly...》《Docker Reported Exit 0...》
|
||||
|
||||
|
||||
* **Layer 2: 持续集成与静态交付 (CI/CD & Hosting)** ➔ 挂载《How I Host This Blog: Astro, Gitea Actions...》
|
||||
|
||||
|
||||
* **Layer 3: AI 算力与记忆存储层 (Local GPU & Memory Stack)** ➔ 挂载《How I Self-Hosted a Mem0 Memory Stack...》
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
4. **双向内链闭环(极为关键)**:
|
||||
* 在每一篇子文章的正文开头(或 `<ExecutiveSummary/>` 底部),自动加上一行面包屑提示:
|
||||
> *🗺️ 本文属于 **[《162 容器生产级自托管基础设施蓝图》](https://www.google.com/search?q=/blueprints/self-hosted-infrastructure)** 的【边缘网关层】实战记录。*
|
||||
|
||||
|
||||
* 这样一来,任何通过长尾搜索(比如搜 `Docker ExitCode 0 OOMKilled false`)进来的技术主管,看完单篇排障后,都会顺着这根藤摸到你的全局蓝图页——**一旦他点进蓝图页,你作为高级全栈/DevOps 工程师的专业形象就彻底立住了。**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 下一步执行建议
|
||||
|
||||
你可以直接把这份回复保存为 `02-顾问落地方案与组件规范.md` 喂给你的 Hermes Agent:
|
||||
|
||||
1. 让它先执行 **Day-1 紧急修复**(清查 `homelab` 字眼、协助你在 GSC 提交 `sitemap-index.xml`、在主站 WordPress 加导航外链)。
|
||||
|
||||
|
||||
2. 让它根据上方代码创建 `ExecutiveSummary.astro` 和 `DynamicCTA.astro` 两个组件,并率先在 **4 篇首页置顶的 Featured 文章** 上完成 Frontmatter 数据补充与上线测试。
|
||||
|
||||
|
||||
|
||||
如果需要在改造 `/about`(升级为 `/work-with-me` 合作与简历二合一页)时帮你设计针对招聘官和企业主的双栏文案,随时把现有的 `about.astro` 源码发给我!
|
||||
@@ -0,0 +1,658 @@
|
||||
# blog.hoelee.com 内容体系与转化漏斗升级 — 顾问补充资料包
|
||||
|
||||
> **对接目标**:针对外部顾问提出的 **6 大升级方向**(双层阅读架构、3 大系统蓝图、视觉证据链、动态 CTA 矩阵、去模板化生成规范、独立求职与合作页面)及 **搜索引擎收录盲区**,提供现场工程代码、典型文章抽样、两篇代表作完整原文及站点关系诊断。
|
||||
> **编制时间**:2026 年 10 月 26 日
|
||||
> **整理人**:Lee Teong Hoe (Mr Hoelee) / Hermes Agent
|
||||
> **站点技术栈**:Astro 5 (Static) + Content Collections + Markdown + Pagefind + Gitea Actions CI/CD + Cloudflare
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
1. [全站结构与核心导航/布局源码](#一全站结构与核心导航布局源码)
|
||||
2. [15 篇代表性文章清单抽样 (Titles & Frontmatter)](#二15-篇代表性文章清单抽样)
|
||||
3. [代表作完整 Markdown 原文(商业案例篇 + 硬核排障篇)](#三代表作完整-markdown-原文)
|
||||
4. [主站与博客的关系及 SEO / GSC 真实状态诊断](#四主站与博客的关系及-seo--gsc-真实状态诊断)
|
||||
5. [期待顾问重点开刀示范的任务清单](#五期待顾问重点开刀示范的任务清单)
|
||||
|
||||
---
|
||||
|
||||
## 一、全站结构与核心导航/布局源码
|
||||
|
||||
### 1. Astro 项目核心目录拓扑
|
||||
```text
|
||||
D:/dev/hoelee-blog/
|
||||
├── src/
|
||||
│ ├── layouts/
|
||||
│ │ └── BaseLayout.astro # 全站唯一基础布局:SEO/JSON-LD/页头Nav/页脚/Umami统计/暗黑模式
|
||||
│ ├── pages/
|
||||
│ │ ├── index.astro # 英文首页(Hero + 置顶精选 4 篇 + 基础设施数据 + 全文列表)
|
||||
│ │ ├── zh/
|
||||
│ │ │ ├── index.astro # 中文首页(与英文版结构镜像)
|
||||
│ │ │ ├── about.astro # 中文关于页
|
||||
│ │ │ ├── posts/index.astro # 中文文章归档页(带客户端过滤)
|
||||
│ │ │ ├── categories/ # 中文分类聚合页与各自分类落地页
|
||||
│ │ │ └── tags/[tag].astro # 中文标签聚合页
|
||||
│ │ ├── posts/
|
||||
│ │ │ ├── [...slug].astro # 英文文章详情动态路由(双语自动匹配映射、TOC、相关文章推荐)
|
||||
│ │ │ └── index.astro # 英文文章归档页
|
||||
│ │ ├── categories/ # 英文分类聚合与 [category].astro
|
||||
│ │ ├── tags/[tag].astro # 英文标签聚合页
|
||||
│ │ ├── about.astro # 英文关于页
|
||||
│ │ ├── 404.astro # 404 页面
|
||||
│ │ ├── robots.txt.ts # 动态生成 robots.txt(纯 ASCII,放行搜索引擎与 AI 爬虫)
|
||||
│ │ ├── rss.xml.ts # RSS 端点
|
||||
│ │ └── llms.txt.ts # 面向 AI 抓取的标准 llms.txt
|
||||
│ ├── components/
|
||||
│ │ ├── AuthorCard.astro # 文章底部实名作者卡片(E-E-A-T 骨架,绑定头像与社媒)
|
||||
│ │ ├── TableOfContents.astro # 侧边/正文目录组件
|
||||
│ │ ├── Search.astro # Pagefind 静态全文搜索弹窗(快捷键 / 触发)
|
||||
│ │ ├── PostList.astro # 文章列表渲染卡片(带缩略图、分类、阅读时长)
|
||||
│ │ ├── Archive.astro # 归档时间轴组件
|
||||
│ │ ├── CategoryGrid.astro # 7 大分类卡片网格
|
||||
│ │ └── CategoryArt.astro # 7 大分类专属矢量 SVG 插画
|
||||
│ ├── content/
|
||||
│ │ └── posts/ # 91 篇英文 Markdown
|
||||
│ │ └── zh/ # 91 篇同名对应的中文 Markdown
|
||||
│ └── styles/
|
||||
│ └── global.css # 全站暗色/亮色 Bento 主题变量与排版样式
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 顶部导航栏(Navbar)设计源码(摘自 `src/layouts/BaseLayout.astro`)
|
||||
```astro
|
||||
<header class="site-header">
|
||||
<nav class="nav" aria-label="Main">
|
||||
<!-- 品牌区:自托管 96px 图标 + 实名 Lee Teong Hoe -->
|
||||
<a class="brand" href={isZh ? '/zh/' : '/'}>
|
||||
<img class="brand-mark" src="/images/hoelee-mark-96.png" alt="" width="26" height="26" decoding="async" />
|
||||
<span class="brand-name">{SITE.author}</span>
|
||||
</a>
|
||||
|
||||
<!-- 导航链接:移动端自动换至第二行 (flex-basis: 100%) 防止挤压功能键 -->
|
||||
<ul class="nav-links">
|
||||
<li><a class="nav-link" href={isZh ? '/zh/posts/' : '/posts/'}>{nav.posts}</a></li>
|
||||
<li><a class="nav-link" href={isZh ? '/zh/categories/' : '/categories/'}>{nav.categories}</a></li>
|
||||
<li><a class="nav-link" href={isZh ? '/zh/about/' : '/about/'}>{nav.about}</a></li>
|
||||
<li><a class="nav-link" href="/rss.xml" title={nav.rss}>{nav.rss}</a></li>
|
||||
</ul>
|
||||
|
||||
<!-- 工具栏:Pagefind 全文搜索 + 中英同文一键双向切换 + 暗/亮模式切换 -->
|
||||
<button class="search-toggle" id="search-toggle" aria-label={isZh ? '搜索' : 'Search'}>
|
||||
⌕ <span class="search-hint">{isZh ? '搜索' : 'Search'}</span><kbd class="search-key">/</kbd>
|
||||
</button>
|
||||
<a class="lang-switch" href={resolvedSwitchHref}>{switchLabel}</a>
|
||||
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle theme">◐</button>
|
||||
</nav>
|
||||
</header>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 首页(`src/pages/index.astro`)核心布局结构
|
||||
```astro
|
||||
<BaseLayout title={SITE.title} description={SITE.description} altLocaleUrl={`${SITE.url}/zh/`}>
|
||||
<section class="hero">
|
||||
<div class="hero-intro">
|
||||
<div class="hero-id">
|
||||
<picture>
|
||||
<source srcset={SITE.photo.smallWebp} type="image/webp" />
|
||||
<img class="hero-photo" src={SITE.photo.small} alt="Lee Teong Hoe" width="72" height="72" />
|
||||
</picture>
|
||||
<h1>Lee Teong Hoe — full-stack developer & DevOps engineer</h1>
|
||||
</div>
|
||||
<p class="hero-sub">
|
||||
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>
|
||||
<div class="hero-pills" aria-label="Core tech stack">
|
||||
<span class="hero-pills-label">Stack:</span>
|
||||
<a class="tech-pill" href="/tags/docker/">Docker</a>
|
||||
<a class="tech-pill" href="/tags/self-hosting/">Self-Hosting</a>
|
||||
<a class="tech-pill" href="/tags/n8n/">n8n</a>
|
||||
<a class="tech-pill" href="/tags/ai/">Local GPU AI</a>
|
||||
<a class="tech-pill" href="/tags/python/">Python</a>
|
||||
<a class="tech-pill" href="/tags/php/">PHP</a>
|
||||
<a class="tech-pill" href="/tags/security/">Security</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基础设施概览卡片与可缩放架构图 -->
|
||||
<div class="hero-visual">
|
||||
<button type="button" class="hero-banner-card" onclick="document.getElementById('arch-lightbox')?.showModal()">
|
||||
<img class="hero-banner-img" src="/images/hero-banner.jpg" alt="Architecture Overview" />
|
||||
<span class="hero-banner-zoom-badge">🔍 View full architecture</span>
|
||||
</button>
|
||||
|
||||
<div class="hero-infra-grid" aria-label="Infrastructure stats">
|
||||
<div class="infra-stat-card"><div class="infra-stat-val">162</div><div class="infra-stat-label">Containers in Prod</div></div>
|
||||
<div class="infra-stat-card"><div class="infra-stat-val">3 Hosts</div><div class="infra-stat-label">Unraid · Synology · VPS</div></div>
|
||||
<div class="infra-stat-card"><div class="infra-stat-val">78</div><div class="infra-stat-label">Compose Stacks</div></div>
|
||||
<div class="infra-stat-card"><div class="infra-stat-val">91</div><div class="infra-stat-label">Bilingual Write-ups</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前三大主行动按钮 -->
|
||||
<div class="hero-actions">
|
||||
<div class="hero-cta">
|
||||
<a class="btn btn-primary" href="#featured">Read the engineering case studies ↓</a>
|
||||
<a class="btn" href="/about/">Hiring? Profile & stack →</a>
|
||||
<a class="btn" href={SITE.website} target="_blank" rel="noopener">Malaysian SME? How I work ↗</a>
|
||||
</div>
|
||||
<p class="hero-note">91 posts in English and 中文 · <a href="/rss.xml">RSS</a></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4 篇置顶旗舰案例(附带量化度量) -->
|
||||
<section class="featured-section" id="featured">
|
||||
<!-- Featured Cards Grid -->
|
||||
</section>
|
||||
|
||||
<!-- 全量文章时间线与分类列表 -->
|
||||
</BaseLayout>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 文章详情页(`src/pages/posts/[...slug].astro`)核心布局结构
|
||||
```astro
|
||||
<BaseLayout title={`${post.data.title} — Mr Hoelee`} ...>
|
||||
<!-- 自动注入 JSON-LD BlogPosting 结构化数据,挂载至站长 Person 实体 -->
|
||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||
|
||||
<article>
|
||||
<header class="article-head">
|
||||
<h1>{post.data.title}</h1>
|
||||
<div class="meta">
|
||||
<time>{fmt(post.data.pubDate)}</time> · <span>{minutes} min read</span> ·
|
||||
<a class="tag" href="...">{post.data.category}</a> {post.data.tags.map(...)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 16:9 响应式 WebP 横幅 -->
|
||||
{post.data.banner && <figure class="article-banner">...</figure>}
|
||||
|
||||
<!-- 自动目录 (TOC) -->
|
||||
<TableOfContents headings={headings} lang={lang} />
|
||||
|
||||
<!-- 正文渲染区 (当前直接渲染完整 Markdown) -->
|
||||
<Content />
|
||||
|
||||
<!-- 实名作者卡片(E-E-A-T 骨架) -->
|
||||
<AuthorCard />
|
||||
</article>
|
||||
|
||||
<!-- 底部 6 卡片相关推荐网格(同语种、高相关性标签智能加权算法) -->
|
||||
<section class="related">...</section>
|
||||
</BaseLayout>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、15 篇代表性文章清单抽样
|
||||
|
||||
覆盖 6 大核心主干,展示真实定位与元数据约束:
|
||||
|
||||
| # | 分类 (`category`) | 标题 (`title`) | 标签 (`tags`) | 摘要 (`description`) |
|
||||
|---|---|---|---|---|
|
||||
| **1** | `case-studies` | **How I Built the DigiKedai Telegram AI Bot** | `[telegram, docker, cloudflare, litellm, typescript, ai]` | A customer-support AI bot that answers questions and provisions free-trial accounts over Telegram and WhatsApp — TypeScript, grammY, a Cloudflare tunnel webhook, and a self-hosted LiteLLM gateway. |
|
||||
| **2** | `case-studies` | **How I Host This Blog: Astro, Gitea Actions, and Self-Hosted CI/CD** | `[astro, gitea, ci-cd, self-hosting, docker, cloudflare]` | A walkthrough of the end-to-end pipeline that builds and serves this site — git-as-CMS, a self-hosted runner on my unRaid server, and Cloudflare in front. |
|
||||
| **3** | `case-studies` | **Self-Healing Access Control for Digital Goods: NocoDB, n8n, and AList** | `[n8n, nocodb, alist, access-control, docker, automation, digital-goods]` | How I built a five-workflow entitlement system that grants, expires, and continuously repairs customer file access — with a build-time code-sharing trick and a public endpoint designed to leak nothing. |
|
||||
| **4** | `case-studies` | **How I Built an Automated Music Video Pipeline with Remotion and Dockerized YouTube CDP** | `[remotion, docker, cdp, youtube, automation, video-generation, whisper]` | Explore an automated music video workflow using Remotion, Whisper, Docker and Chrome DevTools Protocol, from lyric alignment and rendering to YouTube publishing and multilingual subtitles. |
|
||||
| **5** | `case-studies` | **How I Self-Hosted a Mem0 Memory Stack (So My AI Agent Remembers)** | `[mem0, docker, postgres, pgvector, litellm, ai, sharing]` | A private, self-hosted memory layer for AI agents — mem0 API + LiteLLM + PostgreSQL with pgvector on a Synology NAS. Semantic memory under your control, not a third party's. |
|
||||
| **6** | `devops` | **Docker Reported Exit 0. The Kernel Had Already OOM-Killed It.** | `[docker, oom, traefik, synology, crawlers]` | A container on my NAS kept stopping unexpectedly while Docker reported exit 0 and OOMKilled=false. The honest evidence was in dmesg: six cgroup OOM kills. |
|
||||
| **7** | `devops` | **The Forward-Auth Gate That Verified Perfectly — and Wasn't Live** | `[authentik, forward-auth, cloudflare, nginx, docker, self-hosting]` | My authentik forward-auth gate returned a clean 302 to the login page from the host itself, while the public URL still served the app. The hostname had two ingress layers, and I had only changed one. |
|
||||
| **8** | `devops` | **Why Your CDP Clicks Silently Fail: Smooth Scroll, Minimised Windows, and JS-Dispatched Events** | `[cdp, chrome, react, browser-automation, debugging, python]` | Three reasons a Chrome DevTools Protocol click reports success and does nothing: a stale rect from smooth scrolling, a minimised window that drops coordinate input, and a page that received the click but rejected it. |
|
||||
| **9** | `ai` | **Giving AI Your Mouse and Keyboard: Safe Guardrails for Windows Desktop MCP** | `[windows, mcp, security, ai-agents, automation]` | Why running desktop automation MCPs out of the box is dangerous, and the essential guardrails needed to prevent system corruption, UI hangs, and token blowups. |
|
||||
| **10** | `ai` | **I Let an Agent Manage My Shopee Vouchers — And It Created One I Never Approved** | `[ai-agent, shopee, automation, human-in-the-loop, ecommerce, cron]` | Vouchers have no draft state: Confirm means live and drawing on escrow. Here is the audit → propose → auto design I use to keep an agent away from that button, and the list-lag mistake that cost me an unplanned voucher. |
|
||||
| **11** | `engineering` | **Debugging Remotion Video Jitter: Why Bass Scaling Ruins Background Transitions** | `[remotion, react, audio-visualizer, css-animation, debugging]` | Diagnose audio-reactive background jitter and crossfade flicker in Remotion. See how separating the HUD from background layers can improve visual stability and rendering performance. |
|
||||
| **12** | `engineering` | **No API for Browser Translation: Adding an English Mode to a Chinese-Only Web App** | `[i18n, nginx, vue, javascript, self-hosting]` | Browsers expose no JavaScript API to trigger translation — and my app declared English while being Chinese. How I injected an English mode at the reverse proxy. |
|
||||
| **13** | `tutorials` | **Calibrating Intel Quick Sync HEVC: A Cliff at 28, and a Preset That Does Nothing** | `[ffmpeg, hevc, qsv, transcoding, video]` | I measured QSV HEVC on real 1080p files: ICQ 18–22 inflate thin sources, the quality cliff sits between 28 and 30, and fast equals medium byte for byte. |
|
||||
| **14** | `tutorials` | **How to Verify a Hosting Provider Before You Buy** | `[hosting, vps, due-diligence, whois, devops]` | A reusable checklist for separating real hosting claims from marketing: check the domain age against the 'since' claim, read the AUP for silent disqualifiers, and cross-check reputation on third-party sources. |
|
||||
| **15** | `web3` | **An Automated Lottery Contract on Chainlink VRF v2.5 and Automation** | `[solidity, chainlink, vrf, foundry, hardhat, blockchain]` | Building a lottery on Chainlink VRF v2.5 and Automation: why the draw can't be rigged, a full contract walkthrough, and the mock-funding bug that broke it. |
|
||||
|
||||
---
|
||||
|
||||
## 三、代表作完整 Markdown 原文
|
||||
|
||||
### 样本 1(商业交付与 AI 智能体):`how-i-built-the-digikedai-telegram-bot.md`
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "How I Built the DigiKedai Telegram AI Bot"
|
||||
description: "A customer-support AI bot that answers questions and provisions free-trial accounts over Telegram and WhatsApp — TypeScript, grammY, a Cloudflare tunnel webhook, and a self-hosted LiteLLM gateway."
|
||||
pubDate: 2026-09-06
|
||||
category: case-studies
|
||||
tags: [telegram, docker, cloudflare, litellm, typescript, ai]
|
||||
ogImage: /og/how-i-built-the-digikedai-telegram-bot.jpg
|
||||
banner: /banners/how-i-built-the-digikedai-telegram-bot.jpg
|
||||
---
|
||||
|
||||
Digi Kedai sells digital products — online courses, ebooks, and templates — and
|
||||
every sale involves a stream of the same customer questions: *"does this course
|
||||
have a free preview?", "how do I get my account?", "which package should I
|
||||
buy?"*. Answering them manually doesn't scale for a one-person business. So I
|
||||
built an AI customer-support bot that answers those questions around the clock
|
||||
and can even hand out free-trial accounts without a human in the loop.
|
||||
|
||||
This is the story of how it went together — the architecture, the LLM wiring,
|
||||
and the bugs that ate an afternoon each.
|
||||
|
||||
## Why a business needs a bot like this
|
||||
|
||||
Before the architecture, the *why*. A support bot isn't a gimmick — it changes
|
||||
the economics of a small business:
|
||||
|
||||
- **Saves money on staff.** Every routine question a bot answers is one your
|
||||
team doesn't have to. Digi Kedai gets the equivalent of a round-the-clock
|
||||
support agent for a fraction of the cost of hiring one.
|
||||
- **Answers instantly, 24/7.** Customers ask at midnight and on weekends. A bot
|
||||
replies in seconds, in their language — no queues, no "we'll get back to you".
|
||||
- **Converts browsers into buyers.** The bot doesn't just answer — it *upsells*.
|
||||
A "does this have a free trial?" question turns into a claimed trial account
|
||||
in a couple of taps, with no human in the loop.
|
||||
- **Never forgets a customer.** Long-term memory means repeat customers are
|
||||
greeted like regulars, not strangers.
|
||||
- **Scales with your catalogue.** Add a product and the bot already knows it —
|
||||
no retraining, no new FAQ pages.
|
||||
|
||||
For a one-person business like Digi Kedai, that's the difference between losing
|
||||
sales at 2 a.m. and closing them.
|
||||
|
||||
## The stack
|
||||
|
||||
- **TypeScript + Node 20** running as a single container on my Synology NAS.
|
||||
- **grammY** — a lightweight Telegram Bot framework, wired in webhook mode.
|
||||
- **Hono** — a tiny HTTP server that receives webhooks and answers `/health`.
|
||||
- **Cloudflare tunnel** — `bot.digikedai.com` routes straight into the container;
|
||||
no public IP, no open ports.
|
||||
- **LiteLLM** — a self-hosted gateway in front of the actual model, so I can swap
|
||||
providers or add fallbacks without touching bot code.
|
||||
- **PostgreSQL** — conversation and user-memory storage, shared with my mem0 stack.
|
||||
|
||||
## The architecture
|
||||
|
||||
```
|
||||
Customer → Telegram/WhatsApp → Cloudflare tunnel → bot.digikedai.com
|
||||
→ Hono POST /<secret>/webhook → grammY handler
|
||||
→ save to PostgreSQL → Agent.respond (system prompt + retrieved catalog facts + last 20 msgs)
|
||||
→ LiteLLM (model alias "mem0-openai") → persist + reply
|
||||
```
|
||||
|
||||
A few decisions worth explaining:
|
||||
|
||||
**A Cloudflare tunnel into the container, with no Traefik hop.** My homelab is
|
||||
behind CGNAT, so nothing is publicly reachable without a tunnel. The webhook URL
|
||||
lives behind a **secret path segment** (`/<secret>/webhook`) so Telegram's
|
||||
updates only land if the caller knows the secret — a cheap first line of defense
|
||||
on top of Telegram's own token auth.
|
||||
|
||||
**The model is addressed by a LiteLLM *alias*, never a raw model name.** The bot
|
||||
calls `mem0-openai`; LiteLLM maps that to the real model (with an OpenRouter
|
||||
fallback behind it). The bot never needs to know which provider is actually
|
||||
serving the request. I set `temperature: 0.4` and a `45s` timeout.
|
||||
|
||||
**The bot knows its catalogue without prices or internal paths.** A build-time
|
||||
generator turns a single `catalog_sku.csv` (the source of truth) into a
|
||||
TypeScript module the bot imports — SKU, name, category, size, and a product URL,
|
||||
and *nothing else*. Prices are never baked in ("check the site for the current
|
||||
price"), and internal resource paths never ship to the bot image. That keeps the
|
||||
bundle lean and prevents the model from leaking internal structure.
|
||||
|
||||
## Retrieval: how the bot actually *knows* the catalogue
|
||||
|
||||
A generic LLM with the catalogue stuffed into a prompt would hallucinate. So the
|
||||
bot retrieves first, then answers. It has a three-tier retriever:
|
||||
|
||||
1. **SKU token match** — `/\b[A-Z]{2,}\d{2,}\b/gi` catches exact SKUs like `CZH01`.
|
||||
2. **Whole-query substring** — for short, precise queries.
|
||||
3. **Per-segment matching** — splits CJK runs (stripping question particles like
|
||||
有/吗/哪些) and filters English stopwords.
|
||||
|
||||
The top-5 matches become the "facts" injected into the system prompt, and the
|
||||
model is instructed to answer *only* from what it actually retrieved — and to say
|
||||
so when it finds nothing.
|
||||
|
||||
This matters more than it looks. A customer asking *"does CZH01 have a free
|
||||
version?"* gets a correct answer about `FREECZH01` only because the retriever,
|
||||
on hitting the paid SKU, **automatically attaches its free twin** and ranks it
|
||||
second. That one detail turns a "no, sorry" into the correct upsell.
|
||||
|
||||
## Free-trial provisioning — no human in the loop
|
||||
|
||||
The part I'm proudest of: the bot doesn't just *talk about* free trials, it
|
||||
*issues* them. A customer can trigger it three ways — the `/trial` command, a
|
||||
natural-language message like *"我要这个试用:digikedai.com/products/czh01"*, or a
|
||||
deep link like `?start=CZH01`.
|
||||
|
||||
Rather than route through n8n, the bot writes **directly to NocoDB** — inserting
|
||||
a customer row and a customer-product row. My existing webhooks pick that up and
|
||||
provision the actual account automatically, so the bot never touches the file
|
||||
server. The whole thing is idempotent: if a customer already has an account, it
|
||||
reuses it instead of creating a duplicate; if the product insert fails, it rolls
|
||||
back the customer row so a retry doesn't dead-end.
|
||||
|
||||
There's even a multi-account selector. If the same Telegram user has several
|
||||
accounts, the bot lists them as inline buttons and asks which one to attach the
|
||||
trial to.
|
||||
|
||||
## The bugs that ate an afternoon each
|
||||
|
||||
Shipping this was not smooth. Three debugging stories stand out:
|
||||
|
||||
**1. The infinite reply loop.** Telegram re-delivers a webhook update if the
|
||||
server doesn't acknowledge it within a timeout window. My handler was running
|
||||
long (LLM latency), so Telegram re-sent the same message — and the bot answered
|
||||
it again, and again. The fix was `onTimeout: "return"` with a 50-second window,
|
||||
which stops the redelivery spiral dead.
|
||||
|
||||
**2. The model alias 400.** Calling the raw model name (`gpt-5-mini`) returned a
|
||||
400. Only the LiteLLM alias worked. This is now a hard rule in the repo: *the LLM
|
||||
must be the alias, never the raw upstream name.*
|
||||
|
||||
**3. The WhatsApp "m_text" bug.** When I added a WhatsApp channel (via a browser
|
||||
extension that POSTs WhatsApp Web events to a webhook), a "customize webhook
|
||||
payload" setting with empty-string template values made the extension send the
|
||||
literal field *name* `m_text` as the message text — so the bot replied *"I don't
|
||||
understand m_text"*. The fix was to turn that setting off and use the default
|
||||
payload, which the adapter reads correctly.
|
||||
|
||||
The honest takeaway from all three: the failures weren't in the hard parts — the
|
||||
LLM or the retrieval. They were in **webhook lifecycle and payload-contract
|
||||
details**, the boring edges where integrations actually break.
|
||||
|
||||
## Deterministic flows on top of the LLM
|
||||
|
||||
An LLM is great for open-ended questions and terrible for *state*. So the
|
||||
conversational pieces that need reliability — buying, trial redemption, deep
|
||||
links — are **deterministic state machines**, not prompt engineering:
|
||||
|
||||
- **Buy intent** (`order`, `want to buy`, `buy`…) is intercepted before the LLM
|
||||
and drives an inline keyboard (online store → admin → back), never a free-form
|
||||
reply.
|
||||
- **Deep links** carry the SKU in a `?start=` payload, but Telegram only allows
|
||||
`A-Z a-z 0-9 _ -` there — a `:` silently breaks it. I learned that the hard way
|
||||
and switched the separator from `buy:SKU` to `buy-SKU`.
|
||||
- **Off-topic messages** in WhatsApp make the model emit a sentinel (`NO_REPLY`)
|
||||
that the agent turns into *"send nothing"* — so the bot stays silent instead of
|
||||
babbling when it has nothing useful to say.
|
||||
|
||||
## What I'd do differently
|
||||
|
||||
- **Add `setMyCommands()` at startup.** The command list is defined in code, but
|
||||
I never registered it with Telegram, so the in-chat command menu was empty
|
||||
until I fixed it via the Bot API. Small, but it cost a real customer touchpoint.
|
||||
- **Design the payload contract before the channel adapter.** The WhatsApp
|
||||
`m_text` bug came from trusting a customization feature I hadn't read the
|
||||
contract for.
|
||||
- **Treat webhook timeouts as first-class architecture**, not an afterthought —
|
||||
the redelivery loop was avoidable if I'd thought about acknowledgement windows
|
||||
on day one.
|
||||
|
||||
## The result
|
||||
|
||||
One container on the NAS now answers customer questions across two channels
|
||||
(Telegram + WhatsApp), retrieves the correct catalogue entry by SKU or by natural
|
||||
language, and issues free-trial accounts end-to-end — with a prompt suite of
|
||||
~175 unit tests covering the deterministic flows, retrieval, and the payload
|
||||
contracts. The source lives at
|
||||
[git.hoelee.com/hoelee/digikedai-bot](https://git.hoelee.com/hoelee/digikedai-bot).
|
||||
|
||||
If you're building your own Telegram bot backed by an LLM, the lesson is the
|
||||
boring one: the model is the easy part. The webhook lifecycle, the payload
|
||||
contract, and the idempotency of your provisioning are where it actually breaks —
|
||||
design those first.
|
||||
|
||||
---
|
||||
|
||||
## Want a bot like this for your business?
|
||||
|
||||
I build custom Telegram/WhatsApp AI bots, websites, and self-hosted
|
||||
infrastructure for businesses. If a bot like this could save you time and
|
||||
money — or you'd like to hire me — I'd love to talk:
|
||||
|
||||
- 📱 **WhatsApp:** [+60 12-797 2969](https://wa.me/60127972969)
|
||||
- 📧 **Email:** [[email protected]](mailto:[email protected])
|
||||
- 🌐 **Website:** [hoelee.com](https://hoelee.com)
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 样本 2(硬核 DevOps 排障):`docker-reported-exit-0-the-oom-killer-disagreed.md`
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Docker Reported Exit 0. The Kernel Had Already OOM-Killed It."
|
||||
description: "A container on my NAS kept stopping unexpectedly while Docker reported exit 0 and OOMKilled=false. The honest evidence was in dmesg: six cgroup OOM kills."
|
||||
pubDate: 2026-10-16
|
||||
category: devops
|
||||
tags: ["docker", "oom", "traefik", "synology", "crawlers"]
|
||||
ogImage: /og/docker-reported-exit-0-the-oom-killer-disagreed.jpg
|
||||
banner: /banners/docker-reported-exit-0-the-oom-killer-disagreed.jpg
|
||||
draft: false
|
||||
---
|
||||
|
||||
## The notification I expected to be a config mistake
|
||||
|
||||
My NAS sent the notification every Docker user learns to skim: **container
|
||||
stopped unexpectedly**. This one is the edge proxy in front of my
|
||||
self-hosted wiki, and it had been up for two weeks. Nobody had deployed
|
||||
anything that morning and nothing in the compose file had been touched, so
|
||||
the two available stories were "it crashed" and "something restarted it".
|
||||
|
||||
Both were wrong, and the fact that I could not tell from Docker's own report
|
||||
is the part worth writing down.
|
||||
|
||||
## Docker's report says nothing happened
|
||||
|
||||
```text
|
||||
State:
|
||||
Status: running
|
||||
Running: true
|
||||
ExitCode: 0 <- a clean exit
|
||||
OOMKilled: false <- not killed by the kernel
|
||||
FinishedAt: 2026-10-15T19:39:33.63057879Z
|
||||
StartedAt: 2026-10-15T19:39:38.769326672Z <- restarted 5 s later
|
||||
RestartCount: 6
|
||||
RestartPolicy: always
|
||||
```
|
||||
|
||||
Read that as written and the conclusion is: the process exited cleanly, six
|
||||
times, and the restart policy gave it a new life each time. A long-running
|
||||
proxy does not exit with status 0 on its own — and a clean exit that repeats
|
||||
six times is a contradiction, not an answer. `ExitCode` and `OOMKilled` are
|
||||
exactly the two fields I would have trusted here. On this host, both were
|
||||
wrong.
|
||||
|
||||
## The kernel keeps its own record
|
||||
|
||||
On Linux, a cgroup memory kill is logged by the kernel even when the
|
||||
container runtime's bookkeeping does not carry it forward:
|
||||
|
||||
```text
|
||||
[1737797.044856] traefik invoked oom-killer: gfp_mask=0x2400040, order=0, oom_score_adj=0
|
||||
[1737797.123736] Memory cgroup out of memory: Kill process 615 (traefik) score 970 or sacrifice child
|
||||
[1737797.132800] Killed process 615 (traefik) total-vm:2525800kB, anon-rss:478480kB, file-rss:0kB
|
||||
```
|
||||
|
||||
The uptime stamp `1737797` corresponds to 19:39:33 UTC — to the second, the
|
||||
container's `FinishedAt`. That is the whole diagnosis, and it took one
|
||||
`dmesg` to get it. The container did not exit; it was killed inside its own
|
||||
memory limit and restarted 5 seconds later.
|
||||
|
||||
dmesg holds **six** such events, and `RestartCount` was **6**. Every other
|
||||
container in that stack has a restart count of zero and a start time from
|
||||
weeks ago, so the six kills pin to this one container:
|
||||
|
||||
| dmesg uptime | local time | anon-rss at kill |
|
||||
|---|---|---|
|
||||
| 1406539 | Oct 12, ~07:37 | 491 MB |
|
||||
| 1407759 | Oct 12, ~07:57 | 494 MB |
|
||||
| 1432574 | Oct 12, ~14:49 | 486 MB |
|
||||
| 1734793 | Oct 16, ~02:45 | 487 MB |
|
||||
| 1735495 | Oct 16, ~02:58 | 485 MB |
|
||||
| 1737797 | Oct 16, 03:39 ← the notification | 478 MB |
|
||||
|
||||
Every kill happens at 478–494 MB against a limit of 512 MiB (`536,870,912`
|
||||
bytes). That is a ceiling being touched, not a runaway.
|
||||
|
||||
Two more readings made the mechanism clear. The cgroup's recorded peak — the
|
||||
counter that survives the restart — was already back at exactly
|
||||
`536,870,912` **18 minutes** after the restart, i.e. the container touched
|
||||
its ceiling again in the same cycle *without* being killed. And the usage
|
||||
curve oscillated (176 → 308 → 361 MB) instead of climbing monotonically, so
|
||||
this is allocation churn under load hitting a small cap, not a provable slow
|
||||
leak. The prime suspect is a geoblock plugin doing an external HTTPS lookup
|
||||
for every client IP it has not seen before.
|
||||
|
||||
A seventh kill landed at 05:33 that morning, before the fix went in.
|
||||
|
||||
On this DSM build the truth was only in `dmesg` plus the cgroup's
|
||||
`memory.max_usage_in_bytes`. If you are chasing the same shape on a normal
|
||||
host, `docker inspect -f '{{.State.OOMKilled}}'` is the field to read first
|
||||
— it is supposed to be authoritative. Here it read `false` five seconds
|
||||
after the kernel had logged the kill, which is the reason this post exists.
|
||||
|
||||
## What was actually breaking: a crawler flood
|
||||
|
||||
Host memory was never the problem — 64 GB installed, ~39 GB available. One
|
||||
container's 512 MiB ceiling was.
|
||||
|
||||
The driver was traffic, and the numbers came out of the container's own
|
||||
access log. In the last 50,000 lines:
|
||||
|
||||
```text
|
||||
distinct client IPs 25,976
|
||||
403 responses 33,636 (geoblock / deny-list doing its job)
|
||||
429 responses 16,082 (inflight and rate limiting doing their job)
|
||||
container CPU 118–230%
|
||||
host load average 26–32
|
||||
```
|
||||
|
||||
Each restart refilled the heap in 12 to 40 minutes. Nothing in the
|
||||
configuration was wrong, and neither were the 403s and 429s — they are the
|
||||
filter rejecting crawlers exactly as designed. The problem is who is
|
||||
knocking: this container is the public edge for a multi-terabyte Wikipedia
|
||||
corpus served from a home NAS, behind a country allowlist and a bot filter.
|
||||
That is an extremely attractive target for a residential-proxy crawler
|
||||
cloud, and the responses telling it to go away are still work.
|
||||
|
||||
## The fix, and the sentence that has to come with it
|
||||
|
||||
```yaml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
- memory: 512M
|
||||
+ memory: 2g
|
||||
restart: always
|
||||
+memswap_limit: 2g
|
||||
```
|
||||
|
||||
Verification, because "I raised the limit and it feels better" is not a
|
||||
verification:
|
||||
|
||||
- **Only that container was recreated.** The wiki, the proxy core and the
|
||||
robots container still reported two weeks of uptime — the gap users saw
|
||||
was seconds, not a stack restart.
|
||||
- `HostConfig.Memory = 2147483648`, `MemorySwap = 2147483648`, and the
|
||||
cgroup's `memory.limit_in_bytes` agreed with both; `RestartCount` went
|
||||
back to 0.
|
||||
- Peak usage after 18 minutes: **210 MB**, against a ceiling it had been
|
||||
pinning at 512 MB. By midday it sat at 94 MiB.
|
||||
- The mount list still matched the file, including the plugin directory
|
||||
mount — losing that one had once disabled every router in this stack.
|
||||
- Both plugins were loaded and active (country allowlist, bot filter), and
|
||||
the routes returned 200 once the filter's score window expired.
|
||||
|
||||
**And what it is not:** this is a stopgap. The flood is the driver, so an
|
||||
order-of-magnitude bigger flood reaches 2 GB too. The real fix is shrinking
|
||||
the public entry surface, not enlarging the cage around it — and a limit
|
||||
bump that is not labelled as a stopgap becomes a permanent explanation.
|
||||
|
||||
I chose to disable swap for this container rather than let it swap:
|
||||
`memswap_limit` equal to the memory limit does exactly that. The host
|
||||
already had 16 GB of its 40 GB swap in use with committed memory at 92 GB,
|
||||
and an edge proxy that starts swapping slows the entire NAS — the disk array
|
||||
is the bottleneck here, so swapping buys a slower failure rather than a
|
||||
longer life.
|
||||
|
||||
## The trap that nearly made me report a self-inflicted outage
|
||||
|
||||
During verification, my first bare `curl` probes against the site returned
|
||||
**403**, and it looked exactly like a regression I had just caused by
|
||||
restarting the proxy. It was not: the bot filter had scored **my own client
|
||||
IP** (`blocked user-agent`, score 80) and placed it in a 15-minute score
|
||||
window, because I had been hammering the host.
|
||||
|
||||
The tell was in the same test run — `/robots.txt` returned 200 and the
|
||||
internal `/cdn` path returned 400 at the same moment, i.e. the router was
|
||||
healthy and only my own requests were being filtered. On a host where you
|
||||
are also a client, never diagnose with your own bare curl: probe a path the
|
||||
filter ignores, or read a 200 out of the access log.
|
||||
|
||||
## What I would do differently
|
||||
|
||||
1. **Treat "stopped unexpectedly" plus `ExitCode 0` as an instruction to
|
||||
read `dmesg`**, not as evidence of a clean exit. A restart count above
|
||||
zero is a memory investigation until proven otherwise.
|
||||
2. **Alert on the cgroup peak, not only on restarts.** The post-restart
|
||||
reading that was already pinned at the ceiling told me a seventh kill was
|
||||
coming about eighteen minutes before the notification arrived.
|
||||
3. **Get the "who is hitting me" number early.** 25,976 distinct IPs in one
|
||||
log sample is the entire story, and I found it after spending time on the
|
||||
memory curve.
|
||||
4. **Question the container, not the server.** "The server is out of memory"
|
||||
and "one container has a 512 MiB ceiling on a 64 GB machine" need
|
||||
completely different responses, and the second one is invisible unless
|
||||
you look at the limit.
|
||||
|
||||
## The result
|
||||
|
||||
No OOM kill since the change, with a peak at 210 MB after 18 minutes against
|
||||
a ceiling that used to be touched in every single cycle — and the seven
|
||||
kills that had been silently restarting the public edge for four days
|
||||
stopped appearing in dmesg. The crawler flood did not stop, which is why I
|
||||
recorded the failure mode in my ops notes next to the stack: "container
|
||||
stopped unexpectedly" plus `exit 0` / `OOMKilled=false` is a misleading
|
||||
signature, and on this platform the honest evidence lives in the kernel log
|
||||
and the cgroup's high-water mark.
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、主站与博客的关系及 SEO / GSC 真实状态诊断
|
||||
|
||||
### 1. 主站 `hoelee.com` 当前内容与业务定位
|
||||
- **技术载体与形态**:主站是基于 WordPress(Kadence 主题)搭建的企业官网,主域名 `hoelee.com` 301 重定向至 `www.hoelee.com`。
|
||||
- **定位与商业客群**:主打 **Hoelee Enterprise** 商业服务,标语为 *"Revamp Your Online Presence with Website Design & Marketing"*,面向马来西亚中小企业提供企业建站定制、网页设计、在线获客及数字化支持,头部及尾部直接挂载 WhatsApp(+6012-7972969)、直接致电和业务邮箱([email protected])。
|
||||
- **两站断层痛点**:
|
||||
- 博客页头与页脚虽有超链接指向主站 `hoelee.com`,但**主站 6 个主要页面上对博客 `blog.hoelee.com` 的导流入链数为 0**(形成了单向孤岛)。
|
||||
- 主站历史数据库中遗留了约 400 个旧版媒体附件落地页(Thin attachment pages),此前已列入后续整改计划。
|
||||
|
||||
### 2. `blog.hoelee.com` 在 Google Search Console (GSC) 里的真实收录与流量数据
|
||||
- **核实属实**:目前在 Google 自然搜索中几乎没有返回有效结果,流量处于绝对冷启动阶段。
|
||||
- **深层原因剖析**:
|
||||
1. **所有权凭据已具备**:主域名 `hoelee.com` 的 DNS 已经配置了 `google-site-verification` 的 TXT 记录,GSC 的域名级属性(Domain Property)在权限上已经覆盖了整个 `*.hoelee.com`。
|
||||
2. **Sitemap 尚未被 GSC 索引爬取**:Astro 构建产物 `https://blog.hoelee.com/sitemap-index.xml`(内含 `sitemap-0.xml` 包含全部 182 个中英双语页面)与 `robots.txt` 均返回 HTTP 200 且格式严格校验无误;但此前团队精力聚焦在内容创作与音视频流水线构建上,**未在 GSC 控制台后台手动完成最终的提交确认与索引抓取触发**。
|
||||
3. **外链绝对为 0(Cold Start 阻碍)**:这是一个全新上线的独立子域名,除了站内自己的代码仓库外,在外部公网平台(如主站、权威技术社区、LinkedIn、高权重社媒)**没有任何高质量反向链接(Backlinks)**。在没有外链引流权重的冷启动期,Google 爬虫分配的抓取预算极低。
|
||||
4. **自建监控实况**:全站目前通过自建自托管的 **Umami 统计平台(`stats.hoelee.com`)** 进行全量数据监测(已配置防反代 IP 穿透与真实地理位置追踪),目前记录到的均为作者开发调试及直达访问。
|
||||
|
||||
---
|
||||
|
||||
## 五、期待顾问重点开刀示范的任务清单
|
||||
|
||||
请顾问结合上述代码与样本文章,现场示范以下落地规范与组件原型:
|
||||
|
||||
1. **改造示范 1:首屏速览卡片(`<ExecutiveSummary/>`)实例文案**
|
||||
- 针对《How I Built the DigiKedai Telegram AI Bot》与《Docker Reported Exit 0...》,请直接输出两套不同受众侧重点的 Bento 速览卡片文案与字段内容。
|
||||
2. **改造示范 2:正文排版重构与代码折叠(`<TechnicalDeepDive/>`)**
|
||||
- 在上述两篇文章中,指出哪些具体的排障日志、配置代码应当收进折叠区?哪些关键架构与时序图应当提升到前 30% 展示?
|
||||
3. **改造示范 3:高转化动态 CTA 矩阵实战文案**
|
||||
- 替换样本 1 和样本 2 底部千篇一律的 `Want this for your business?`,针对 AI 自动化客户与 DevOps 稳定性客户,分别给出具有极强点击冲动的专属 CTA 钩子。
|
||||
4. **架构落地指导:3 大系统蓝图(Pillar Pages)的最佳路由与挂载规划**
|
||||
- 在 Astro 现有路由(`src/pages/`)下,这 3 大蓝图页建议作为独立的顶层路由(如 `/blueprints/infra`、`/blueprints/ai-agents`)还是整合进分类页?如何最大化提升整站 Topic Authority?
|
||||
Reference in new issue
Block a user