# 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")**: ```plaintext 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`) ```plaintext 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: `/` and `//`. | 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: ```text tag:k8s "container image" category:tech cgroup ``` ### Search Syntax Capabilities - **Schema Filters**: - `tag:` or `tags:` (e.g. `tag:k8s`) - `category:` or `cat:` (e.g. `category:tech`) - `author:` - `lang:` - **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 `...`. - **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//`: ```javascript // 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 ``; }, injectPostEnd({ post }) { return `
Custom author bio or reaction buttons
`; }, // 5. Lifecycle event: new comment posted async onCommentCreated({ comment, post }) {} }; ``` --- ## 7. Bilingual Post Authoring In `/app/content/posts//`: ### Default Language (`index.md`) ```yaml --- 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`) ```yaml --- title: "使用 Gemini-CLI 与 MCP 服务整理 Dropbox" lang: zh --- Post content in Chinese... ``` ### Routing - `https://pengzhan.dev/` → Serves default version (`index.md`). - `https://pengzhan.dev//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: ```bash # 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 ```bash docker build -t pengzhan-web:latest . ``` ### Run Container with Mounted Persistent Volume ```bash docker run -d \ -p 3000:3000 \ -v /path/to/my-content:/app/content \ --name pengzhan-site \ pengzhan-web:latest ``` ### Kubernetes Deployment Example ```yaml 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 ```