- Editorial web platform built with Node.js, Express, EJS, and Vanilla CSS - Markdown content engine with YAML frontmatter, bilingual variants, and hot-reload watcher - Unified SQLite with FTS5 search indexing, query logging, and schema filters - Centered editorial post layout with margin-docked left TOC and mobile bottom sheet - WordPress-grade comment system with moderation dashboard and bulk operations under /guanli - Complete SEO suite (JSON-LD structured data, RSS/Atom feeds, XML sitemap, robots.txt) - CI/CD workflows for linting, security audits (Gitleaks), automated tests, and Docker releases
247 lines
8.6 KiB
Markdown
247 lines
8.6 KiB
Markdown
# 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: `/<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:
|
|
|
|
```text
|
|
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>/`:
|
|
|
|
```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 `<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`)
|
|
|
|
```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/<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 & Deployment
|
|
|
|
### Run Locally
|
|
|
|
```bash
|
|
# 1. Install dependencies
|
|
npm install
|
|
|
|
# 2. Run automated test suite (6/6 suites)
|
|
npm test
|
|
|
|
# 3. Start local development server (uses ./sample-content by default)
|
|
npm start
|
|
# Server runs at http://localhost:3000
|
|
```
|
|
|
|
### 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
|
|
```
|