feat(architecture): implement consultant review r4 (stream separation, nav upgrade, internal cta, schema & anchor fixes)
Deploy / build (push) Successful in 25s

This commit is contained in:
hoelee committed 2026-10-27 01:19:38 +08:00
1 parent cc28722c79
commit 419c962e2f
31 files changed
+286 -42

No files matched your search

+4
View File
@@ -60,6 +60,10 @@ const postDates = new Map();
export default defineConfig({
site: 'https://blog.hoelee.com',
trailingSlash: 'always',
redirects: {
'/work-with-me': '/about/',
'/zh/work-with-me': '/zh/about/',
},
i18n: {
defaultLocale: 'en',
locales: ['en', 'zh'],
+7 -1
View File
@@ -71,6 +71,11 @@ summary:
# [V2 Feature] High-Conversion Dynamic CTA Matrix (Rendered automatically at bottom)
dynamicCta: true
# [V2 Feature] Homepage Stream Separation & Blueprint Mounting
stream: engineering # engineering (default for main list) | creative-lab (GPU/multimodal lab strip)
blueprint: ai-agent-automation # Optional: self-hosted-infrastructure | ai-agent-automation | sme-digital-suite
blueprintLayer: L1-deterministic-orchestration # Optional: L1..L4 layer slug
---
```
@@ -93,9 +98,10 @@ Every technical post follows the 5-phase "Hard Job → Post" narrative:
3. **The Fix / Architecture**: The exact runnable configuration, patch, or state-machine code.
4. **Technical Deep Dive Accordions**:
- For long code dumps, verbose config files, or raw logs (>20 lines), wrap them in native HTML `<details>`:
- **Summary Specification**: `<summary>` MUST state the exact deliverable/artifact and line count/scope (e.g. `🔍 View production docker-compose.yml (Traefik v3 + 2GB mem_limit, 42 lines)`). Never use vague text like `View configuration` or `点击展开`.
```html
<details class="tech-deep-dive">
<summary>🔍 查看完整的 Docker Compose 配置与网络策略</summary>
<summary>🔍 查看完整的 Docker Compose 配置文件与网络策略 (Traefik v3 + 2GB 内存限制,42 行)</summary>
```yaml
services:
+7 -1
View File
@@ -71,6 +71,11 @@ summary:
# [V2 Feature] High-Conversion Dynamic CTA Matrix (Rendered automatically at bottom)
dynamicCta: true
# [V2 Feature] Homepage Stream Separation & Blueprint Mounting
stream: engineering # engineering (default for main list) | creative-lab (GPU/multimodal lab strip)
blueprint: ai-agent-automation # Optional: self-hosted-infrastructure | ai-agent-automation | sme-digital-suite
blueprintLayer: L1-deterministic-orchestration # Optional: L1..L4 layer slug
---
```
@@ -93,9 +98,10 @@ Every technical post follows the 5-phase "Hard Job → Post" narrative:
3. **The Fix / Architecture**: The exact runnable configuration, patch, or state-machine code.
4. **Technical Deep Dive Accordions**:
- For long code dumps, verbose config files, or raw logs (>20 lines), wrap them in native HTML `<details>`:
- **Summary Specification**: `<summary>` MUST state the exact deliverable/artifact and line count/scope (e.g. `🔍 View production docker-compose.yml (Traefik v3 + 2GB mem_limit, 42 lines)`). Never use vague text like `View configuration` or `点击展开`.
```html
<details class="tech-deep-dive">
<summary>🔍 查看完整的 Docker Compose 配置与网络策略</summary>
<summary>🔍 查看完整的 Docker Compose 配置文件与网络策略 (Traefik v3 + 2GB 内存限制,42 行)</summary>
```yaml
services:
+2 -1
View File
@@ -3,6 +3,7 @@ import type { CollectionEntry } from 'astro:content';
import { getCollection } from 'astro:content';
import { postLang } from '../lib/lang';
import { tagCounts, tagLink } from '../lib/tags';
import { categoryLabel } from '../lib/categories';
interface Props {
posts: CollectionEntry<'posts'>[];
@@ -74,7 +75,7 @@ const thumb = (post: CollectionEntry<'posts'>, w: 400 | 800) => {
</>
)}
<span class="sep">·</span>
<a class="tag" href={catHref(post.data.category)}>{post.data.category}</a>
<a class="tag" href={catHref(post.data.category)}>{categoryLabel(post.data.category, lang)}</a>
{post.data.tags.map((t) => {
const href = tagLink(t, counts, lang);
return href ? <a class="tag" href={href}>{t}</a> : <span class="tag">{t}</span>;
+3
View File
@@ -41,6 +41,9 @@ const posts = defineCollection({
}).optional(),
}).optional(),
dynamicCta: z.boolean().default(false),
stream: z.enum(['engineering', 'creative-lab']).default('engineering'),
blueprint: z.enum(['self-hosted-infrastructure', 'ai-agent-automation', 'sme-digital-suite']).optional(),
blueprintLayer: z.string().optional(),
}),
});
@@ -4,6 +4,7 @@ description: "Diagnose audio-reactive background jitter and crossfade flicker in
pubDate: 2025-11-26
updatedDate: 2026-10-22
category: engineering
stream: creative-lab
tags: ["remotion", "react", "audio-visualizer", "css-animation", "debugging"]
ogImage: "/og/debugging-remotion-video-jitter.jpg"
banner: "/banners/debugging-remotion-video-jitter.jpg"
@@ -3,6 +3,7 @@ title: "From Audio to a 4-Minute Music Video: Orchestrating Gemini Argon 4, AI D
description: "A complete production breakdown of building a 4-minute 1080p anime OP music video (Crescent Bay): AI music generation, Gemini Argon 4 storyboard prompting, iterative image-to-video rerolls, and Filmora VFX editing."
pubDate: 2026-10-25
category: case-studies
stream: creative-lab
tags: ["ai-video", "music-video", "gemini", "filmora", "face-swapping", "ai-music", "creative-workflows"]
ogImage: "/og/from-audio-to-a-4-minute-ai-music-video.jpg"
banner: "/banners/from-audio-to-a-4-minute-ai-music-video.jpg"
@@ -3,6 +3,7 @@ title: "Generating Full-Length Songs with YuE2 BF16: Overcoming Duration Limits
description: "AI music tools cut off complex tracks mid-sentence. Here is how I solved the 240s duration wall using YuE2 BF16, 295s headroom budgeting, and a 60-song pipeline."
pubDate: 2026-10-26
category: tutorials
stream: creative-lab
tags: ["ai-music", "yue2", "comfyui", "audio-engineering", "subtitles", "python"]
ogImage: "/og/generating-full-length-songs-with-yue2-comfyui.jpg"
banner: "/banners/generating-full-length-songs-with-yue2-comfyui.jpg"
@@ -3,6 +3,7 @@ title: "Generating Subtitles for Songs That Ship Without Timestamps"
description: "AI music tools give you audio and nothing else — no line timings, no karaoke. Here is the pipeline I use to produce LRC and SRT subtitles for 18 songs, why plain transcription fails, and the two bugs that made the lyrics box scroll to the bottom."
pubDate: 2026-10-20
category: tutorials
stream: creative-lab
tags: ["subtitles", "lrc", "srt", "whisper", "forced-alignment", "ai-music", "python"]
ogImage: "/og/generating-subtitles-for-songs-without-timestamps.jpg"
banner: "/banners/generating-subtitles-for-songs-without-timestamps.jpg"
@@ -4,6 +4,7 @@ description: 'Explore an automated music video workflow using Remotion, Whisper,
pubDate: 2025-04-18
updatedDate: 2026-10-22
category: case-studies
stream: creative-lab
tags: ["remotion", "docker", "cdp", "youtube", "automation", "video-generation", "whisper"]
ogImage: "/og/how-i-built-an-automated-music-video-pipeline.jpg"
banner: "/banners/how-i-built-an-automated-music-video-pipeline.jpg"
@@ -3,6 +3,7 @@ title: "How I Made My Own Songs with Suno AI"
description: "I wrote the lyrics, tuned the prompts, and shipped seven songs with Suno AI — folk ballads, a kids' tune, an anime-style Heart Sutra. Full workflow + the tracks."
pubDate: 2026-09-11
category: case-studies
stream: creative-lab
tags: [suno, ai-music, music, lyrics, prompting, ai]
ogImage: /og/how-i-made-my-own-songs-with-suno-ai.jpg
banner: /banners/how-i-made-my-own-songs-with-suno-ai.jpg
@@ -3,6 +3,7 @@ title: "Running AI Video Locally on a 12 GB GPU: Real Numbers for Wan 2.2 and Se
description: "Measured on an RTX 3060: Wan 2.2 TI2V-5B generation, SeedVR2 upscaling, the 1080p resolution trap, and the one command that fixed five failed runs."
pubDate: 2026-10-20
category: ai
stream: creative-lab
tags: [ai-video, wan, comfyui, seedvr2, local-llm, gpu, cost]
ogImage: /og/running-ai-video-locally-on-a-12gb-gpu.jpg
banner: /banners/running-ai-video-locally-on-a-12gb-gpu.jpg
@@ -3,6 +3,7 @@ title: "Why My AI Songs Kept Losing Their Endings: Running YuE2 on a 12 GB GPU"
description: "Twelve Mandarin songs came out exactly 240.000 seconds long with the outros missing. The cause wasn't the lyrics — it was a hard cap, a VRAM ceiling, and a planner that ignores BPM."
pubDate: 2026-10-20
category: ai
stream: creative-lab
tags: ["yue2", "comfyui", "ai-music", "local-ai", "gpu", "vram", "mandarin"]
ogImage: "/og/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.jpg"
banner: "/banners/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.jpg"
@@ -4,6 +4,7 @@ description: "深入排查 Remotion 音频可视化中背景画面剧烈抽搐
pubDate: 2025-11-26
updatedDate: 2026-10-22
category: engineering
stream: creative-lab
tags: ["remotion", "react", "audio-visualizer", "css-animation", "debugging"]
ogImage: "/og/zh-debugging-remotion-video-jitter.jpg"
banner: "/banners/debugging-remotion-video-jitter.jpg"
@@ -47,7 +47,7 @@ RestartPolicy: always
照字面读,结论就是:进程干净退出了六次,每次都由重启策略续了命。一个长期运行的代理不会自己以状态 0 退出——而「干净退出」重复六次本身是矛盾,不是答案。`ExitCode` 和 `OOMKilled` 恰好是我在这里最信任的两个字段。在这台机器上,两个都是错的。
## 内核自己留了另一份记录
## 内核自己留了另一份记录 <a id="the-kernel-keeps-its-own-record"></a>
在 Linux 上,cgroup 的内存击杀由内核记录,哪怕容器运行时的账本没有把它带过来:
@@ -124,13 +124,13 @@ dmesg 里这样的记录一共 **6 条**,而 `RestartCount` 正是 **6**。同
线索就在同一轮测试里:`/robots.txt` 返回 200,内部的 `/cdn` 路径返回 400,而两者是同一时刻——说明路由是健康的,只有我自己的请求被过滤。在一台「你同时是客户端」的宿主上,永远不要用裸 curl 做判断:去探一个过滤器会忽略的路径,或者从访问日志里读出一个 200。
## 如果重来一次
## 如果重来一次 <a id="what-i-would-do-differently"></a>
1. **把「意外停止」加 `ExitCode 0` 当成「去读 dmesg」的指令**,而不是干净退出的证据。重启计数大于零的容器,在证明清白之前都当作内存事故来查。
2. **对 cgroup 高水位告警,而不是只对重启告警。** 重启后那个已经贴着天花板的读数,在通知到达前约十八分钟就告诉我第 7 次击杀要来了。
3. **早点拿到「谁在打我」这个数字。** 一份日志样本里 25,976 个不同 IP 就是全部故事,而我是在看完整条内存曲线之后才找到它。
4. **怀疑容器,别怀疑服务器。** 「服务器内存不够」和「一台 64 GB 的机器上某个容器只有 512 MiB 上限」需要完全不同的应对,而后者你不去看那个上限就根本看不见。
## 结果
## 结果 <a id="the-result"></a>
改动之后再没有出现 OOM 击杀,18 分钟后的峰值是 210 MB,而过去每一个周期都会摸到上限——四天里一直悄悄重启公网入口的那七次击杀,也从 dmesg 里消失了。爬虫洪水并没有停,所以我把这个失败形态记进了那套 stack 的运维笔记:**「容器意外停止」+ `exit 0` / `OOMKilled=false` 是一个会误导人的签名**,而在这类平台上,诚实的证据住在内核日志和 cgroup 的高水位里。
@@ -3,6 +3,7 @@ title: "从一首歌到 4 分钟完整 MV:我如何用 Gemini、AI 生成与 F
description: "基于《月牙湾》1080P 高清动漫 OP 制作实录:详解 AI 音乐重构、Gemini Argon 4 提示词分镜规划、文字生图与图生视频多次抽卡调优,以及 Filmora 剪辑与特效合成的真实生产实践。"
pubDate: 2026-10-25
category: case-studies
stream: creative-lab
tags: ["ai-video", "music-video", "gemini", "filmora", "face-swapping", "ai-music", "creative-workflows"]
ogImage: "/og/zh-from-audio-to-a-4-minute-ai-music-video.jpg"
banner: "/banners/from-audio-to-a-4-minute-ai-music-video.jpg"
@@ -3,6 +3,7 @@ title: "用 YuE2 BF16 生成完整长歌:突破时长限制与音频硬截断"
description: "AI 音乐工具常在歌词中途戛然而止。本文详解如何利用 YuE2 BF16、295秒余量预留公式与本地 ComfyUI,成功批量跑出 60 首自然收尾的完整长歌全集。"
pubDate: 2026-10-26
category: tutorials
stream: creative-lab
tags: ["ai-music", "yue2", "comfyui", "audio-engineering", "subtitles", "python"]
ogImage: "/og/zh-generating-full-length-songs-with-yue2-comfyui.jpg"
banner: "/banners/generating-full-length-songs-with-yue2-comfyui.jpg"
@@ -3,6 +3,7 @@ title: "给没有时间戳的歌曲自动生成字幕"
description: "AI 音乐工具只给你音频 —— 没有逐行时间,没有卡拉 OK。这是我给 18 首歌生成 LRC 与 SRT 字幕的完整流程,为什么单纯转写不行,以及让歌词框滚到底的两个 bug。"
pubDate: 2026-10-20
category: tutorials
stream: creative-lab
tags: ["subtitles", "lrc", "srt", "whisper", "forced-alignment", "ai-music", "python"]
ogImage: "/og/zh-generating-subtitles-for-songs-without-timestamps.jpg"
banner: "/banners/generating-subtitles-for-songs-without-timestamps.jpg"
@@ -4,6 +4,7 @@ description: '基于 Remotion、Whisper、Docker 与 Chrome DevTools Protocol
pubDate: 2025-04-18
updatedDate: 2026-10-22
category: case-studies
stream: creative-lab
tags: ["remotion", "docker", "cdp", "youtube", "automation", "video-generation", "whisper"]
ogImage: "/og/zh-how-i-built-an-automated-music-video-pipeline.jpg"
banner: "/banners/how-i-built-an-automated-music-video-pipeline.jpg"
@@ -28,7 +28,7 @@ Digi Kedai 销售数字产品——在线课程、电子书、模板——每一
这篇讲的是它是怎么搭起来的——架构、LLM 接线,以及那些各自吃掉我一整个下午的 bug。
## 为什么企业需要一个这样的机器人
## 为什么企业需要一个这样的机器人 <a id="why-a-business-needs-a-bot-like-this"></a>
讲架构之前,先说*为什么*。客服机器人不是噱头——它改变了小生意的经济学:
@@ -49,7 +49,7 @@ Digi Kedai 销售数字产品——在线课程、电子书、模板——每一
- **LiteLLM**——一个位于真实模型之前的自托管网关,这样我可以不碰机器人代码就切换供应商或加故障转移。
- **PostgreSQL**——对话与用户记忆存储,与我的 mem0 栈共用。
## 架构
## 架构 <a id="the-architecture"></a>
```
顾客 → Telegram/WhatsApp → Cloudflare 隧道 → bot.digikedai.com
@@ -112,7 +112,7 @@ LLM 擅长开放式问题,却很不擅长*状态*。所以那些需要可靠
- **在写通道适配器之前先定好 payload 契约。** WhatsApp 的 `m_text` bug 就来自我信任了一个没读过契约的自定义功能。
- **把 webhook 超时当作一等架构**,而不是事后补丁——如果我在第一天就想到确认窗口,重投递循环本可以避免。
## 结果
## 结果 <a id="the-result"></a>
现在 NAS 上的一个容器就能跨两个通道(Telegram + WhatsApp)解答顾客问题,按 SKU 或自然语言检索到正确商品,并端到端发放免费试用账号——还有一套约 175 个单元测试覆盖确定性流程、检索和 payload 契约。源码在
[git.hoelee.com/hoelee/digikedai-bot](https://git.hoelee.com/hoelee/digikedai-bot)。
@@ -3,6 +3,7 @@ title: "我用 Suno AI 做出自己的歌"
description: "自己写词、调风格提示词,用 Suno AI 做出七首原创歌曲——民谣、儿歌、动漫风心经。完整工作流 + 可直接播放的成品。"
pubDate: 2026-09-11
category: case-studies
stream: creative-lab
tags: [suno, ai-music, music, lyrics, prompting, ai]
ogImage: /og/zh-how-i-made-my-own-songs-with-suno-ai.jpg
banner: /banners/how-i-made-my-own-songs-with-suno-ai.jpg
@@ -3,6 +3,7 @@ title: "12GB 显卡跑本地 AI 视频:Wan 2.2 加 SeedVR2 的真实数据"
description: "在 RTX 3060 上实测:Wan 2.2 TI2V-5B 生成、SeedVR2 放大、1080p 分辨率陷阱,以及那条让连续五次失败全部通过的命令。"
pubDate: 2026-10-20
category: ai
stream: creative-lab
tags: [ai-video, wan, comfyui, seedvr2, local-llm, gpu, cost]
ogImage: /og/zh-running-ai-video-locally-on-a-12gb-gpu.jpg
banner: /banners/running-ai-video-locally-on-a-12gb-gpu.jpg
@@ -3,6 +3,7 @@ title: "为什么我的 AI 歌曲总是丢结尾:在 12GB 显卡上跑 YuE2"
description: "12 首中文歌时长全都恰好 240.000 秒,尾奏不见了。原因不在歌词 —— 是一道硬上限、一堵显存天花板,和一个不看 BPM 的规划器。"
pubDate: 2026-10-20
category: ai
stream: creative-lab
tags: ["yue2", "comfyui", "ai-music", "local-ai", "gpu", "vram", "mandarin"]
ogImage: "/og/zh-yue2-on-a-12gb-gpu-songs-that-lost-their-endings.jpg"
banner: "/banners/yue2-on-a-12gb-gpu-songs-that-lost-their-endings.jpg"
+4 -4
View File
@@ -62,8 +62,8 @@ const switchLabel = isZh ? 'EN' : '中文';
// Locale-aware nav labels
const nav = isZh
? { posts: '文章', categories: '分类', about: '关于与合作', rss: 'RSS' }
: { posts: 'Posts', categories: 'Categories', about: 'About & Hire', rss: 'RSS' };
? { caseStudies: '工程案例', posts: '全部文章', about: '关于与合作' }
: { caseStudies: 'Case Studies', posts: 'All Posts', about: 'Work With Me' };
---
<!doctype html>
@@ -184,10 +184,9 @@ const nav = isZh
whole group onto a second row (`flex-basis: 100%`) instead of letting
individual links wrap around the search / language / theme controls. */}
<ul class="nav-links">
<li><a class="nav-link" href={isZh ? '/zh/categories/case-studies/' : '/categories/case-studies/'}>{nav.caseStudies}</a></li>
<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>
<button class="search-toggle" id="search-toggle" aria-label={isZh ? '搜索' : 'Search'}>
⌕ <span class="search-hint">{isZh ? '搜索' : 'Search'}</span><kbd class="search-key">/</kbd>
@@ -204,6 +203,7 @@ const nav = isZh
<footer class="site-footer">
<span>© {new Date().getFullYear()} {SITE.author} · {SITE.handle}</span>
<span>
<a href="/rss.xml">RSS</a>
<a href={SITE.linkedin} target="_blank" rel="noopener">LinkedIn</a>
<a href={SITE.github} target="_blank" rel="noopener">GitHub</a>
<a href={SITE.gitea} target="_blank" rel="noopener">Gitea</a>
+14 -7
View File
@@ -41,7 +41,7 @@ export const CATEGORY_META: Record<CategorySlug, CategoryMeta> = {
},
'case-studies': {
slug: 'case-studies',
name: { en: 'Case Studies', zh: '案例研究' },
name: { en: 'Case Studies', zh: '工程案例' },
blurb: {
en: 'How I built X, end to end: the problem, the debugging story, the fix and the measured result. This is my working portfolio.',
zh: '「我是如何构建 X」的完整实录:问题、踩坑、修复与可衡量的结果。这是我的实战作品集。',
@@ -50,16 +50,16 @@ export const CATEGORY_META: Record<CategorySlug, CategoryMeta> = {
},
devops: {
slug: 'devops',
name: { en: 'DevOps', zh: 'DevOps' },
name: { en: 'DevOps', zh: '运维架构' },
blurb: {
en: 'Docker & Portainer, Traefik, Cloudflare tunnels, a ~140-container NAS homelab, backups and monitoring — my most hands-on, differentiated material.',
zh: 'Docker/Portainer、Traefik、Cloudflare 隧道、约 140 个容器的 NAS 家庭机房、备份与监控——我最硬核、实操最深的领域。',
en: 'Docker & Portainer, Traefik, Cloudflare tunnels, 162 production containers across 3 hosts, backups and monitoring — my most hands-on, differentiated material.',
zh: 'Docker/Portainer、Traefik、Cloudflare 隧道、三宿主 162 个生产容器、备份与监控——我最硬核、实操最深的领域。',
},
accent: '#295cff',
},
engineering: {
slug: 'engineering',
name: { en: 'Engineering', zh: '工程开发' },
name: { en: 'Engineering', zh: '系统工程' },
blurb: {
en: 'Java & Spring, PHP and WordPress deep dives — how production web applications are really put together, from the code up.',
zh: 'Java/Spring、PHP 与 WordPress 深度解析——生产级 Web 应用从代码层面开始的真实构建方式。',
@@ -68,7 +68,7 @@ export const CATEGORY_META: Record<CategorySlug, CategoryMeta> = {
},
notes: {
slug: 'notes',
name: { en: 'Notes', zh: '随记' },
name: { en: 'Notes', zh: '排障手记' },
blurb: {
en: 'Short, low-friction entries: fixes, gotchas and link roundups from day-to-day work. Quick reads that save someone an afternoon.',
zh: '简短速记:问题修复、避坑记录与链接精选,来自日常开发的第一手经验。三分钟读完,省一个下午。',
@@ -77,7 +77,7 @@ export const CATEGORY_META: Record<CategorySlug, CategoryMeta> = {
},
tutorials: {
slug: 'tutorials',
name: { en: 'Tutorials', zh: '教程' },
name: { en: 'Tutorials', zh: '实战教程' },
blurb: {
en: 'Beginner-friendly, follow-along how-tos. Copy the commands, run them, get a working result — then understand why it worked.',
zh: '面向初学者的跟练教程:复制命令、动手运行、得到可用结果——再弄懂它为什么能work。',
@@ -101,4 +101,11 @@ export const ALL_CATEGORIES: CategoryMeta[] = CATEGORY_SLUGS.map((s) => CATEGORY
/** Safer lookup that tolerates unknown strings (e.g. old post data). */
export function categoryMeta(slug: string): CategoryMeta | undefined {
return (CATEGORY_META as Record<string, CategoryMeta | undefined>)[slug];
}
/** Get localized display label for a category slug. */
export function categoryLabel(slug: string, lang: 'en' | 'zh' = 'en'): string {
const meta = categoryMeta(slug);
if (!meta) return slug;
return meta.name[lang] ?? meta.name.en ?? slug;
}
+2 -2
View File
@@ -64,7 +64,7 @@ const writeUps = (await getCollection('posts', isEn)).length;
<div class="dual-track-grid">
<!-- Track A: For Business Owners & SMEs -->
<div class="track-card track-business">
<div class="track-card track-business" id="sme">
<div class="track-header">
<span class="track-badge">🏢 For Business Owners &amp; Founders</span>
<h3>Custom Web Systems, AI Automation &amp; Self-Hosted Infrastructure</h3>
@@ -102,7 +102,7 @@ const writeUps = (await getCollection('posts', isEn)).length;
</div>
<!-- Track B: For Tech Leads & Recruiters -->
<div class="track-card track-hiring">
<div class="track-card track-hiring" id="hiring">
<div class="track-header">
<span class="track-badge">👨‍💻 For Hiring Managers &amp; Tech Leads</span>
<h3>Full-Stack, DevOps &amp; AI Systems Engineering Roles</h3>
+46 -5
View File
@@ -7,6 +7,8 @@ import { SITE } from '../config';
import { sortForListing } from '../lib/sort';
const posts = sortForListing(await getCollection('posts', isEn));
const engineeringPosts = posts.filter((p) => p.data.stream !== 'creative-lab');
const creativeLabPosts = posts.filter((p) => p.data.stream === 'creative-lab');
// Featured picks: fixed slugs, not "newest 4", so the front page stays stable after a
// publish and keeps answering "who is this and what has he actually run" (scale →
@@ -112,7 +114,7 @@ const featuredMetrics: Record<string, string> = {
<div class="hero-cta">
<a class="btn btn-primary" href="#featured">Read the engineering case studies ↓</a>
<a class="btn" href="/about/">Hiring? Profile &amp; stack →</a>
<a class="btn" href={SITE.website} target="_blank" rel="noopener">Malaysian SME? How I work ↗</a>
<a class="btn" href="/about/#sme">Malaysian SME? How I work ↗</a>
</div>
<p class="hero-note">
{posts.length} posts in English and 中文 · <a href="/rss.xml">RSS</a>
@@ -179,7 +181,7 @@ const featuredMetrics: Record<string, string> = {
<section>
<div class="section-head-with-nav">
<h2>Latest posts</h2>
<h2>Latest engineering &amp; DevOps posts</h2>
<div class="section-nav-pills" aria-label="Category shortcuts">
<a class="section-nav-pill" href="/categories/case-studies/">case-studies</a>
<a class="section-nav-pill" href="/categories/devops/">devops</a>
@@ -188,11 +190,50 @@ const featuredMetrics: Record<string, string> = {
<a class="section-nav-pill section-nav-all" href="/posts/">All {posts.length} →</a>
</div>
</div>
{posts.length === 0 ? (
{engineeringPosts.length === 0 ? (
<p><em>No posts yet — check back soon.</em></p>
) : (
<PostList posts={posts} limit={6} />
<PostList posts={engineeringPosts} limit={6} />
)}
<p class="archive-meta"><a href="/posts/">All {posts.length} posts</a> · <a href="/tags/docker/">tag pages</a></p>
</section>
{creativeLabPosts.length > 0 && (
<section class="creative-lab-strip">
<div class="lab-strip-header">
<div class="lab-strip-title-wrap">
<span class="lab-strip-badge">⚡ GPU Node</span>
<h2>Local GPU &amp; Multimodal Pipelines (RTX 3060 Lab)</h2>
</div>
<p class="lab-strip-sub">
High-throughput audio synthesis, forced-alignment subtitling, and diffusion workflows running on our self-hosted GPU node.
</p>
</div>
<div class="lab-cards-grid">
{creativeLabPosts.slice(0, 3).map((post) => {
const cleanSlug = post.slug.replace(/^zh\//, '');
return (
<a class="lab-card" href={`/posts/${post.slug}/`}>
<div class="lab-card-thumb">
<img
src={`/og/thumb/${cleanSlug}-400.webp`}
alt=""
loading="lazy"
decoding="async"
width="400"
height="210"
onerror="this.style.display='none'"
/>
</div>
<div class="lab-card-body">
<span class="lab-card-tag">{post.data.category}</span>
<h3>{post.data.title}</h3>
<p>{post.data.description}</p>
</div>
</a>
);
})}
</div>
<p class="archive-meta"><a href="/posts/">All {posts.length} posts</a> · <a href="/tags/docker/">tag pages</a></p>
</section>
)}
</BaseLayout>
+14 -4
View File
@@ -8,6 +8,7 @@ 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 { categoryLabel } from '../../lib/categories';
import { SITE } from '../../config';
export async function getStaticPaths() {
@@ -57,12 +58,12 @@ const translation = post.data.translation
const switchHref = translation ? `/posts/${translation}/` : undefined;
const altLocaleUrl = translation ? new URL(`/posts/${translation}/`, SITE.url).href : undefined;
// BlogPosting structured data — gives Google a rich-result-eligible node and
// BlogPosting / TechArticle structured data — gives Google a rich-result-eligible node and
// links the article to the site-wide Person node (E-E-A-T).
const postUrl = new URL(`/posts/${post.slug}/`, SITE.url).href;
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
'@type': post.data.summary ? 'TechArticle' : 'BlogPosting',
mainEntityOfPage: { '@type': 'WebPage', '@id': postUrl },
headline: post.data.title,
description: post.data.description,
@@ -76,6 +77,15 @@ const jsonLd = {
articleSection: post.data.category,
keywords: post.data.tags.join(', '),
wordCount: words,
...(post.data.summary
? {
dependencies: post.data.summary.stack.join(', '),
audience: {
'@type': 'Audience',
audienceType: post.data.summary.audience,
},
}
: {}),
};
---
@@ -103,7 +113,7 @@ const jsonLd = {
<span class="sep">·</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>
<a class="tag" href={lang === 'zh' ? `/zh/categories/${post.data.category}/` : `/categories/${post.data.category}/`}>{categoryLabel(post.data.category, lang)}</a>
{post.data.tags.map((t) => {
const href = tagLink(t, tagCount, lang);
return href ? <a class="tag" href={href}>{t}</a> : <span class="tag">{t}</span>;
@@ -154,7 +164,7 @@ const jsonLd = {
decoding="async"
/>
<div class="related-body">
<span class="related-badge">{p.data.category}</span>
<span class="related-badge">{categoryLabel(p.data.category, lang)}</span>
<h3>{p.data.title}</h3>
<time datetime={p.data.pubDate.toISOString()}>
{fmt(p.data.pubDate)}
+2 -2
View File
@@ -63,7 +63,7 @@ const writeUps = (await getCollection('posts', isZh)).length;
<div class="dual-track-grid">
<!-- 轨道 A: 面向企业主与创业者 -->
<div class="track-card track-business">
<div class="track-card track-business" id="sme">
<div class="track-header">
<span class="track-badge">🏢 致企业主与业务负责人</span>
<h3>网站与业务系统定制 · 24/7 AI 获客客服 · 私有化部署降本</h3>
@@ -100,7 +100,7 @@ const writeUps = (await getCollection('posts', isZh)).length;
</div>
<!-- 轨道 B: 面向技术主管与招聘方 -->
<div class="track-card track-hiring">
<div class="track-card track-hiring" id="hiring">
<div class="track-header">
<span class="track-badge">👨‍💻 致技术主管与招聘方</span>
<h3>全栈开发、DevOps 运维与 AI 系统架构工程角色</h3>
+51 -9
View File
@@ -5,8 +5,11 @@ import { getCollection } from 'astro:content';
import { isZh } from '../../lib/lang';
import { SITE } from '../../config';
import { sortForListing } from '../../lib/sort';
import { categoryLabel } from '../../lib/categories';
const posts = sortForListing(await getCollection('posts', isZh));
const engineeringPosts = posts.filter((p) => p.data.stream !== 'creative-lab');
const creativeLabPosts = posts.filter((p) => p.data.stream === 'creative-lab');
// Same four topics as the English front page, in their Chinese versions, so the two
// front pages tell the same story (scale → automation → observability → a delivered
@@ -113,7 +116,7 @@ const featuredMetrics: Record<string, string> = {
<div class="hero-cta">
<a class="btn btn-primary" href="#featured">先看工程案例 ↓</a>
<a class="btn" href="/zh/about/">招聘方:技术栈与履历 →</a>
<a class="btn" href={SITE.website} target="_blank" rel="noopener">中小企业:合作方式 ↗</a>
<a class="btn" href="/zh/about/#sme">中小企业:合作方式 ↗</a>
</div>
<p class="hero-note">
共 {posts.length} 篇中文文章 · <a href="/">English</a> · <a href="/rss.xml">RSS</a>
@@ -162,7 +165,7 @@ const featuredMetrics: Record<string, string> = {
</div>
<div class="bento-content">
<div class="bento-top-meta">
<span class="featured-badge">{post.data.category}</span>
<span class="featured-badge">{categoryLabel(post.data.category, 'zh')}</span>
<span class="bento-metric-pill">
<span class="pulse-dot"></span>
{metric}
@@ -180,20 +183,59 @@ const featuredMetrics: Record<string, string> = {
<section>
<div class="section-head-with-nav">
<h2>最新文章</h2>
<h2>最新生产与运维文章</h2>
<div class="section-nav-pills" aria-label="分类快捷入口">
<a class="section-nav-pill" href="/zh/categories/case-studies/">工程案例</a>
<a class="section-nav-pill" href="/zh/categories/devops/">运维</a>
<a class="section-nav-pill" href="/zh/categories/tutorials/">教程</a>
<a class="section-nav-pill" href="/zh/categories/notes/">笔记</a>
<a class="section-nav-pill" href="/zh/categories/devops/">运维架构</a>
<a class="section-nav-pill" href="/zh/categories/tutorials/">实战教程</a>
<a class="section-nav-pill" href="/zh/categories/notes/">排障手记</a>
<a class="section-nav-pill section-nav-all" href="/zh/posts/">全部 {posts.length} 篇 →</a>
</div>
</div>
{posts.length === 0 ? (
{engineeringPosts.length === 0 ? (
<p><em>中文文章即将发布,敬请期待。</em></p>
) : (
<PostList posts={posts} lang="zh" limit={6} />
<PostList posts={engineeringPosts} lang="zh" limit={6} />
)}
<p class="archive-meta"><a href="/zh/posts/">全部 {posts.length} 篇</a></p>
</section>
{creativeLabPosts.length > 0 && (
<section class="creative-lab-strip">
<div class="lab-strip-header">
<div class="lab-strip-title-wrap">
<span class="lab-strip-badge">⚡ 本地 GPU 算力</span>
<h2>本地 GPU 算力与多模态工程实验室</h2>
</div>
<p class="lab-strip-sub">
基于本地 RTX 3060 生产节点的语音克隆、强制对齐字幕与音视频批量生成管线实录。
</p>
</div>
<div class="lab-cards-grid">
{creativeLabPosts.slice(0, 3).map((post) => {
const cleanSlug = post.slug.replace(/^zh\//, '');
return (
<a class="lab-card" href={`/posts/${post.slug}/`}>
<div class="lab-card-thumb">
<img
src={`/og/thumb/${cleanSlug}-400.webp`}
alt=""
loading="lazy"
decoding="async"
width="400"
height="210"
onerror="this.style.display='none'"
/>
</div>
<div class="lab-card-body">
<span class="lab-card-tag">{categoryLabel(post.data.category, 'zh')}</span>
<h3>{post.data.title}</h3>
<p>{post.data.description}</p>
</div>
</a>
);
})}
</div>
<p class="archive-meta"><a href="/zh/posts/">全部 {posts.length} 篇</a></p>
</section>
)}
</BaseLayout>
+108
View File
@@ -1539,3 +1539,111 @@ a.category-card:hover .art img {
.search-hint, .search-key { display: none; }
.author-card { flex-direction: column; align-items: flex-start; }
}
/* ---------- Creative Lab Strip (GPU & Multimodal Lab) ---------- */
.creative-lab-strip {
margin-top: 3.5rem;
padding: 1.75rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.lab-strip-header {
margin-bottom: 1.5rem;
}
.lab-strip-title-wrap {
display: flex;
align-items: center;
gap: 0.65rem;
margin-bottom: 0.4rem;
}
.lab-strip-badge {
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 600;
color: #ff7ab6;
background: rgba(255, 122, 182, 0.12);
border: 1px solid rgba(255, 122, 182, 0.3);
border-radius: 9999px;
padding: 0.15rem 0.6rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.lab-strip-header h2 {
margin: 0;
font-size: 1.35rem;
}
.lab-strip-sub {
color: var(--muted);
font-size: 0.95rem;
margin: 0;
max-width: 46rem;
line-height: 1.5;
}
.lab-cards-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.25rem;
margin-bottom: 1.25rem;
}
.lab-card {
display: flex;
flex-direction: column;
background: var(--surface-subtle, rgba(255, 255, 255, 0.02));
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
text-decoration: none;
color: inherit;
transition: transform 0.15s ease, border-color 0.15s ease;
}
.lab-card:hover {
transform: translateY(-2px);
border-color: var(--brand);
}
.lab-card-thumb {
width: 100%;
aspect-ratio: 1200 / 630;
overflow: hidden;
background: var(--surface);
line-height: 0;
}
.lab-card-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.lab-card-body {
padding: 1rem;
display: flex;
flex-direction: column;
flex: 1;
}
.lab-card-tag {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--faint);
margin-bottom: 0.4rem;
}
.lab-card-body h3 {
font-size: 1rem;
line-height: 1.35;
margin: 0 0 0.5rem;
color: var(--ink);
}
.lab-card-body p {
font-size: 0.85rem;
line-height: 1.45;
color: var(--muted);
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
@media (max-width: 768px) {
.lab-cards-grid {
grid-template-columns: 1fr;
}
}