2026-09-16 23:41:33 -07:00

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.yaml immediately hot-reloads in memory via chokidar.
  • 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 /search page 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> or tags:<name> (e.g. tag:k8s)
    • category:<name> or cat:<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 /blogs and /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 dev or make dev automatically runs with APP_ENV=staging.
  • Ephemeral Sandbox: In staging or test mode (NODE_ENV=test), server/services/db.js mirrors the real SQLite database into a temporary sandbox in os.tmpdir(). You get 100% full application functionality, but all writes are discarded on exit, keeping sample-content/data/site.db 100% 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
S
Description
No description provided
Readme
64 MiB
Languages
JavaScript 49.8%
EJS 33.9%
CSS 14.7%
Makefile 1.4%
Dockerfile 0.2%