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:
@@ -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.
|
||||
|
||||

|
||||
---
|
||||
|
||||
## 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>` → 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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user