feat: initial release of pengzhan.dev personal editorial blog platform

- 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
This commit is contained in:
2026-09-16 23:08:53 -07:00
parent ac2c4c09ef
commit 8c0b04db0e
126 changed files with 8515 additions and 3984 deletions
+216 -36
View File
@@ -1,66 +1,246 @@
# Resume template
# pengzhan.dev — Dynamic Web Framework & Personal Site Skeleton
*A simple Jekyll + GitHub Pages powered resume template.*
A custom, ultra-lightweight dynamic web framework designed to replicate and replace WordPress for **pengzhan.dev**, optimized for containerized deployment on Kubernetes.
![img](images/screenshot.png)
---
## Docs
## 1. Architectural Model: Skeleton vs. Persistent Volume
### Running locally
The architecture separates the **web runtime container (the "Skeleton")** from your **content and extensions (the "Persistent Volume")**:
To test locally, run the following in your terminal:
```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) │
└───────────────────────────────────────────────┘
```
1. Clone repo locally
1. `bundle install`
2. `bundle exec jekyll serve`
3. Open your browser to `localhost:4000`
- **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).
### Running locally with Docker
---
To test locally with docker, run the following in your terminal after installing docker into your system:
## 2. Directory Structure on Persistent Volume (`/app/content`)
1. `docker image build -t resume-template .`
2. `docker run --rm --name resume-template -v "$PWD":/home/app --network host resume-template`
```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)
```
### Customizing
---
First you'll want to fork the repo to your own account. Then clone it locally and customize, or use the GitHub web editor to customize.
## 3. Posts vs. Pages
#### Options/configuration
| 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. |
Most of the basic customization will take place in the `/_config.yml` file. Here is a list of customizations available via `/_config.yml`:
---
[...write these out...]
## 4. 100% Dynamic Taxonomy (Tags & Categories)
#### Editing content
- **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.
Most of the content configuration will take place in the `/_layouts/resume.html` file. Simply edit the markup there accordingly
---
### Publishing to GitHub Pages for free
## 5. Structured Schema Search Engine
[GitHub Pages](https://pages.github.com/) will host this for free with your GitHub account. Just make sure you're using a `gh-pages` branch, and the site will automatically be available at `yourusername.github.io/resume-template` (you can rename the repo to resume for your own use if you want it to be available at `yourusername.github.io/resume`). You can also add a CNAME if you want it to be available at a custom domain...
The built-in search engine supports a full query DSL combining metadata filters, exact phrases, and free-text keywords:
### Configuring with your own domain name
```text
tag:k8s "container image" category:tech cgroup
```
To setup your GH Pages site with a custom domain, [follow the instructions](https://help.github.com/articles/setting-up-a-custom-domain-with-github-pages/) on the GitHub Help site for that topic.
### Search Syntax Capabilities
### Themes
- **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.
Right now resume-template only has one theme. More are coming :soon: though. :heart:
---
## Roadmap
## 6. Plugin & Extension Architecture (`plugins/`)
A feature roadmap is [available here](https://github.com/jglovier/resume-template/projects/1). If you features suggestions, please [open a new issue](https://github.com/jglovier/resume-template/issues/new).
Plugins are self-contained folders placed in `/app/content/plugins/<plugin-name>/`:
## Contributing
```javascript
// plugins/my-plugin/index.js
module.exports = {
// 1. Initialize Express middleware or routes
onInit({ app, config, pluginConfig }) {},
If you spot a bug, or want to improve the code, or even make the dummy content better, you can do the following:
// 2. Extend markdown-it renderer (e.g. custom fences, math, diagrams)
onMarkdownInit({ markdownIt, pluginConfig }) {},
1. [Open an issue](https://github.com/jglovier/resume-template/issues/new) describing the bug or feature idea
2. Fork the project, make changes, and submit a pull request
// 3. Transform or augment post metadata
onPostLoad({ post, pluginConfig }) {},
## License
// 4. Inject into HTML head or body
injectHead({ req, pageType, post }) {
return `<script src="..."></script>`;
},
The code and styles are licensed under the MIT license. [See project license.](LICENSE) Obviously you should not use the content of this demo repo in your own resume. :wink:
injectPostEnd({ post }) {
return `<div>Custom author bio or reaction buttons</div>`;
},
Disclaimer: Use of Lisa M. Simpson image and name used under [Fair Use](https://en.wikipedia.org/wiki/Fair_use) for educational purposes. Project license does not apply to use of this material.
// 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 & 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
```