d865ee2d2b3289475d01fd10c5b399b22355b4aa
Build and Push Docker Image / build (push) Successful in 1m5s
pengzhan.dev — Dynamic Web Framework & Personal Site Skeleton
A custom, ultra-lightweight dynamic web framework designed to replicate and replace WordPress for pengzhan.dev, optimized for containerized deployment on Kubernetes.
1. Architectural Model: Skeleton vs. Persistent Volume
The architecture separates the web runtime container (the "Skeleton") from your content and extensions (the "Persistent Volume"):
Kubernetes Pod
┌───────────────────────────────────────────────┐
│ Docker Container (The "Skeleton") │
│ - Node.js 22 + Express │
│ - SSR EJS Templates │
│ - Markdown-it + PrismJS (Okaidia) │
│ - Structured Schema Search Engine │
│ - Dynamic Plugin Loader │
│ - Hot-reload Watcher (Chokidar) │
└───────────────────────┬───────────────────────┘
│ mounts /app/content
▼
┌───────────────────────────────────────────────┐
│ Persistent Volume (Your Content & Data) │
│ - posts/ (Blog articles & bilinguals) │
│ - pages/ (Standalone timeless pages) │
│ - plugins/ (Custom executable plugins) │
│ - static/ (Fonts, PDFs, Images) │
│ - css/ (custom.css overrides) │
│ - data/ (site.db SQLite) │
│ - config.yaml (Site metadata & settings) │
└───────────────────────────────────────────────┘
- Zero Container Restarts: Editing markdown files, uploading PDFs/images, adding new pages, or changing
config.yamlimmediately hot-reloads in memory viachokidar. - Tiny Footprint: Multi-stage Docker image weighs ~25MB (no React/Vue hydration overhead, no headless browser, no heavy DB daemon).
2. Directory Structure on Persistent Volume (/app/content)
content/
├── config.yaml # Site title, tagline, social links, author bio
├── posts/ # Blog articles
│ ├── using-gemini-cli-and-mcp-server-to-organize-my-dropbox/
│ │ ├── index.md # Default post (English or Chinese)
│ │ ├── zh.md # Chinese translation variant
│ │ └── images/ # Post assets
│ │ └── cover.png
│ └── kubelet-source-code-deep-dive/
│ └── index.md
├── pages/ # Standalone timeless pages
│ ├── home.md # Homepage / About-me (mapped to /)
│ ├── quick-links.md # Quick links page (mapped to /quick-links)
│ └── resume.md # Custom standalone page (mapped to /resume)
├── plugins/ # Executable plugins & extensions
│ ├── google-analytics/
│ │ ├── plugin.json
│ │ └── index.js
│ └── reading-time/
│ ├── plugin.json
│ └── index.js
├── static/ # Static files served directly at root
│ ├── fonts/ # XiaoheSimplifySerif-VF, Maple Mono NF CN
│ ├── documents/ # CV.pdf, research papers
│ └── images/ # Favicon, profile avatars
├── css/
│ └── custom.css # User CSS loaded after base styles
└── data/
└── site.db # SQLite database (auto-created by server)
3. Posts vs. Pages
| Feature | posts/ |
pages/ |
|---|---|---|
| Role | Time-stamped, categorized blog articles. | Timeless, standalone site destinations (e.g. Home, Quick Links). |
| Routing | Dedicated blog router: /<slug> and /<slug>/<lang>. |
Direct top-level route based on filename or permalink (/quick-links, /resume). |
| Taxonomy | Dynamically feeds category, tags: [...], and author metadata. |
No tags or dates required; does not pollute blog streams. |
| Stream & RSS | Appears in /blogs stream, pagination, and /feed (RSS 2.0). |
Excluded from blog streams, pagination, and RSS feeds. |
| Bilingual | Sibling translation files (index.md + zh.md / en.md) with in-page switcher. |
Self-contained markdown or HTML with custom layout control. |
4. 100% Dynamic Taxonomy (Tags & Categories)
- Zero Static Configuration: Tags are never declared statically in
config.yaml. - Whenever a post specifies
tags: [k8s, cloud, "AI Era"], the content engine dynamically discovers, normalizes, and aggregates it with count statistics. - The
/searchpage automatically derives its default discovery pills from the top active tags and categories.
5. Structured Schema Search Engine
The built-in search engine supports a full query DSL combining metadata filters, exact phrases, and free-text keywords:
tag:k8s "container image" category:tech cgroup
Search Syntax Capabilities
- Schema Filters:
tag:<name>ortags:<name>(e.g.tag:k8s)category:<name>orcat:<name>(e.g.category:tech)author:<name>lang:<en|zh>
- Exact Phrases: Quoted strings (
"container image") requiring exact substring match. - Keywords: Bare words searched across title, tags, excerpt, and full markdown body.
- Ranking & Highlighting: Returns relevance-scored results with contextual snippets wrapped in
<mark>...</mark>. - Deduplication: Bilingual versions of the same post are unified so you never get duplicate cards for the same post.
6. Plugin & Extension Architecture (plugins/)
Plugins are self-contained folders placed in /app/content/plugins/<plugin-name>/:
// plugins/my-plugin/index.js
module.exports = {
// 1. Initialize Express middleware or routes
onInit({ app, config, pluginConfig }) {},
// 2. Extend markdown-it renderer (e.g. custom fences, math, diagrams)
onMarkdownInit({ markdownIt, pluginConfig }) {},
// 3. Transform or augment post metadata
onPostLoad({ post, pluginConfig }) {},
// 4. Inject into HTML head or body
injectHead({ req, pageType, post }) {
return `<script src="..."></script>`;
},
injectPostEnd({ post }) {
return `<div>Custom author bio or reaction buttons</div>`;
},
// 5. Lifecycle event: new comment posted
async onCommentCreated({ comment, post }) {}
};
7. Bilingual Post Authoring
In /app/content/posts/<post-slug>/:
Default Language (index.md)
---
title: "Using Gemini-CLI and MCP server to organize my dropbox"
date: 2025-08-31
author: "haopengzhan"
category: "Tech"
tags: ["AI Era", "Dropbox", "Gemini", "k8s"]
excerpt: "How I automated Dropbox organization with MCP."
lang: en
---
Post content in English...
Translation Variant (zh.md)
---
title: "使用 Gemini-CLI 与 MCP 服务整理 Dropbox"
lang: zh
---
Post content in Chinese...
Routing
https://pengzhan.dev/<post-slug>→ Serves default version (index.md).https://pengzhan.dev/<post-slug>/zh→ Serves Chinese version (zh.md).- Both are linked via the in-page language switcher pill (
🌐 EN | 中文). - Only one entry appears in
/blogsand/search.
8. Development & Makefile Workflows
The platform includes a native Makefile designed to protect your production data while developing and testing:
# 1. Install dependencies
npm install
# 2. Run local development in Safe Ephemeral Staging Mode (Default)
make dev
# -> Copies site.db to a temp file in /tmp so your real database is NEVER mutated.
# -> Discards all test queries and mock comments automatically on exit.
# 3. Run development in Persistent Mode (only when you intentionally want to save changes to site.db)
make dev-persistent
# 4. Run automated test suite (in isolated temp DB)
make test
# 5. Run project linter
make lint
# 6. Clean temporary staging artifacts
make clean
Database Isolation Architecture
- Safe by Default: Running
npm run devormake devautomatically runs withAPP_ENV=staging. - Ephemeral Sandbox: In staging or test mode (
NODE_ENV=test),server/services/db.jsmirrors the real SQLite database into a temporary sandbox inos.tmpdir(). You get 100% full application functionality, but all writes are discarded on exit, keepingsample-content/data/site.db100% pristine.
Docker Build
docker build -t pengzhan-web:latest .
Run Container with Mounted Persistent Volume
docker run -d \
-p 3000:3000 \
-v /path/to/my-content:/app/content \
--name pengzhan-site \
pengzhan-web:latest
Kubernetes Deployment Example
apiVersion: apps/v1
kind: Deployment
metadata:
name: pengzhan-web
spec:
replicas: 1
template:
spec:
containers:
- name: web
image: pengzhan-web:latest
ports:
- containerPort: 3000
volumeMounts:
- name: content
mountPath: /app/content
volumes:
- name: content
persistentVolumeClaim:
claimName: pengzhan-content-pvc
Languages
JavaScript
49.8%
EJS
33.9%
CSS
14.7%
Makefile
1.4%
Dockerfile
0.2%