Files
pengzhan.dev/README.md
T
haopengzhan 95a7dcb93b feat(admin): add ephemeral staging DB, read-only posts overview, and dynamic search
- Implement ephemeral staging mode (APP_ENV=staging) to isolate test/dev DB writes in /tmp
- Add read-only posts management dashboard under /guanli/posts with live links and comment counters
- Add hot re-indexing endpoint (/guanli/reindex) to refresh Markdown without restarting server
- Replace static recommended search config with dynamic frequency-based trending searches
- Remove early scaffold mockup file (dropbox/zh.md) and clean legacy mock test queries
- Add native Makefile with dev, dev-persistent, test, lint, and docker targets
- Update test suite and documentation for development workflows
2026-09-16 23:09:14 -07:00

261 lines
9.5 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>` &rarr; Serves default version (`index.md`).
- `https://pengzhan.dev/<post-slug>/zh` &rarr; 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
```