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
-3
View File
@@ -1,3 +0,0 @@
---
BUNDLE_PATH: "_vendor/bundle"
BUNDLE_DISABLE_SHARED_GEMS: "true"
+5 -5
View File
@@ -1,7 +1,7 @@
.git
node_modules/
npm-debug.log
.git/
.gitignore
_site
.sass-cache
.agent/
sample-content/
.DS_Store
_vendor
*.log
+44
View File
@@ -0,0 +1,44 @@
name: CI Quality & Security Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
security-scan:
name: Gitleaks Secret Scan
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
test-and-lint:
name: Lint & Automated Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Code Linter
run: npm run lint
- name: Run Automated Test Suites
run: npm test
+52
View File
@@ -0,0 +1,52 @@
name: Docker Build & Release
on:
push:
branches: [ main ]
tags: [ 'v*.*.*' ]
jobs:
build-and-push:
name: Build & Push Container Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU (Multi-platform emulation)
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry (ghcr.io)
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=sha,format=short,prefix=sha-
type=semver,pattern={{version}}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+17 -3
View File
@@ -1,4 +1,18 @@
_site
.sass-cache
# Dependencies
node_modules/
# OS and Environment
.DS_Store
_vendor
.env
.agent/
# Logs
*.log
npm-debug.log*
# SQLite Database & WAL / Journal files (runtime persistent state)
*.db
*.db-journal
*.db-wal
*.db-shm
**/data/*.db*
-11
View File
@@ -1,11 +0,0 @@
language: ruby
rvm:
- 2.5.0
script: bundle exec jekyll build
install: bundle install
notifications:
email: false
-1
View File
@@ -1 +0,0 @@
resume-template.joelglovier.com
+24 -6
View File
@@ -1,11 +1,29 @@
FROM ruby:2.7
# Multi-stage lightweight build for pengzhan.dev skeleton
FROM node:22-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
WORKDIR /home/app
FROM node:22-alpine AS runner
WORKDIR /app
COPY Gemfile* ./
ENV NODE_ENV=production
ENV PORT=3000
RUN bundle install
# Copy node_modules from dependencies stage
COPY --from=dependencies /app/node_modules ./node_modules
COPY package*.json ./
COPY . .
# Copy skeleton web framework code
COPY server/ ./server/
COPY views/ ./views/
COPY public/ ./public/
CMD [ "bundle", "exec", "jekyll", "serve" ]
# Persistent Volume mount point
VOLUME /app/content
EXPOSE 3000
USER node
CMD ["node", "server/index.js"]
-8
View File
@@ -1,8 +0,0 @@
source "https://rubygems.org"
gem 'github-pages'
# this gem provides regeneration support improvements on Windows
gem 'wdm', '>= 0.1.0' if Gem.win_platform?
gem "webrick"
gem "rexml", "~> 3.2"
-281
View File
@@ -1,281 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.0.6.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concurrent-ruby (1.2.0)
dnsruby (1.61.3)
addressable (~> 2.5)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
ethon (0.15.0)
ffi (>= 1.15.0)
eventmachine (1.2.7)
eventmachine (1.2.7-x64-mingw32)
execjs (2.8.1)
faraday (1.10.2)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
ffi (1.15.5)
ffi (1.15.5-x64-mingw32)
forwardable-extended (2.6.0)
gemoji (3.0.1)
github-pages (205)
github-pages-health-check (= 1.16.1)
jekyll (= 3.8.7)
jekyll-avatar (= 0.7.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.6)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.13.0)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.13.0)
jekyll-mentions (= 1.5.1)
jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.3.0)
jekyll-redirect-from (= 0.15.0)
jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.1)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.6.1)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.1.1)
jekyll-theme-cayman (= 0.1.1)
jekyll-theme-dinky (= 0.1.1)
jekyll-theme-hacker (= 0.1.1)
jekyll-theme-leap-day (= 0.1.1)
jekyll-theme-merlot (= 0.1.1)
jekyll-theme-midnight (= 0.1.1)
jekyll-theme-minimal (= 0.1.1)
jekyll-theme-modernist (= 0.1.1)
jekyll-theme-primer (= 0.5.4)
jekyll-theme-slate (= 0.1.1)
jekyll-theme-tactile (= 0.1.1)
jekyll-theme-time-machine (= 0.1.1)
jekyll-titles-from-headings (= 0.5.3)
jemoji (= 0.11.1)
kramdown (= 1.17.0)
liquid (= 4.0.3)
mercenary (~> 0.3)
minima (= 2.5.1)
nokogiri (>= 1.10.4, < 2.0)
rouge (= 3.13.0)
terminal-table (~> 1.4)
github-pages-health-check (1.16.1)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (~> 4.0)
public_suffix (~> 3.0)
typhoeus (~> 1.3)
html-pipeline (2.14.3)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jekyll (3.8.7)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (~> 1.14)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.7.0)
jekyll (>= 3.0, < 5.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.3.1)
commonmarker (~> 0.14)
jekyll (>= 3.7, < 5.0)
jekyll-commonmark-ghpages (0.1.6)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1.2)
rouge (>= 2.0, < 4.0)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.13.0)
jekyll (>= 3.7, < 5.0)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.13.0)
jekyll (>= 3.4, < 5.0)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.5.1)
html-pipeline (~> 2.3)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
jekyll (>= 3.0, < 5.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
jekyll-redirect-from (0.15.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-remote-theme (0.4.1)
addressable (~> 2.0)
jekyll (>= 3.5, < 5.0)
rubyzip (>= 1.3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0)
jekyll-theme-architect (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.4)
jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.11.1)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
kramdown (1.17.0)
liquid (4.0.3)
listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6)
mini_portile2 (2.8.1)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.17.0)
multipart-post (2.1.1)
nokogiri (1.14.1)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.14.1-x64-mingw32)
racc (~> 1.4)
octokit (4.22.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (3.1.1)
racc (1.6.2)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.6)
rouge (3.13.0)
ruby-enum (0.8.0)
i18n
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.11)
thread_safe (~> 0.1)
unicode-display_width (1.7.0)
webrick (1.8.1)
zeitwerk (2.6.6)
PLATFORMS
ruby
x64-mingw32
DEPENDENCIES
github-pages
rexml (~> 3.2)
webrick
BUNDLED WITH
2.1.4
-22
View File
@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Joel Glovier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+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
```
-1848
View File
File diff suppressed because one or more lines are too long
-17
View File
@@ -1,17 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="Dribbble" fill="#D1CECC" d="M13.76,14.4c0.37-0.15,0.78-0.26,1.16-0.43c-0.18-0.34-0.35-0.73-0.5-1.04
c-0.06-0.11-0.1-0.25-0.2-0.33c-2.49,0.72-5.25,1.17-8.48,1.16c-0.06,2.6,0.88,4.41,2.12,5.79C9.3,17.33,11.22,15.46,13.76,14.4z
M15.45,15.38c-2.87,1.04-5.13,2.68-6.53,5.19c0.95,0.74,2.22,1.38,3.72,1.65c1.65,0.29,3.38-0.01,4.6-0.55
c-0.37-2.12-0.97-4.14-1.65-5.92C15.57,15.62,15.57,15.46,15.45,15.38z M10.47,6.52c-2.3,1.15-3.92,2.99-4.58,5.79
c2.87-0.04,5.42-0.39,7.66-1.04C12.58,9.64,11.6,8.01,10.47,6.52z M14.01,0C6.27,0,0,6.28,0,14.01s6.27,14.01,14.01,14.01
c7.05,0,12.89-5.21,13.86-12c0.1-0.46,0.14-0.94,0.13-1.43c0.01-0.2,0.01-0.39,0.01-0.58C28.02,6.28,21.75,0,14.01,0z M21.17,20.6
c-0.77,0.85-1.69,1.53-2.78,2.07c-1.09,0.55-2.33,0.97-3.8,1.04c-3.1,0.15-5.52-1.04-7.14-2.53c-1.66-1.53-2.92-3.59-3.14-6.56
c-0.11-1.58,0.2-2.97,0.67-4.15c0.48-1.23,1.06-2.14,1.84-3.01c1.11-1.2,2.61-2.26,4.5-2.78c0.68-0.19,1.39-0.31,2.1-0.36
c3.17-0.2,5.51,1.07,7.16,2.56c1.65,1.48,2.94,3.62,3.11,6.58C23.89,16.57,22.7,18.94,21.17,20.6z M17.05,14.98
c0.64,1.86,1.23,3.76,1.59,5.9c0.46-0.26,0.86-0.61,1.24-0.99c1.14-1.15,1.99-2.63,2.33-4.55C20.79,14.91,18.79,14.7,17.05,14.98z
M15.2,10.8c1.73-0.7,3.28-1.6,4.3-3.01c-1.36-1.14-3.32-2.18-5.82-2.05c-0.6,0.03-1.05,0.08-1.59,0.2
c0.93,1.37,1.93,2.79,2.73,4.22C14.94,10.38,15.02,10.63,15.2,10.8z M15.81,12.06c0.2,0.38,0.39,0.81,0.56,1.19
c0.06,0.14,0.08,0.31,0.2,0.41c1.93-0.22,4.06-0.06,5.74,0.28c-0.06-2.27-0.82-3.85-1.87-5.14C19.24,10.23,17.65,11.27,15.81,12.06z
"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-7
View File
@@ -1,7 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="Facebook" fill="#D1CECC" d="M14,0C6.27,0,0,6.27,0,14s6.27,14,14,14s14-6.27,14-14S21.73,0,14,0z M20.99,20.22
c0,0.43-0.35,0.77-0.77,0.77h-3.56v-5.42h1.82l0.27-2.11h-2.09v-1.35c0-0.61,0.17-1.03,1.05-1.03l1.12,0V9.2
c-0.19-0.03-0.86-0.08-1.63-0.08c-1.61,0-2.71,0.98-2.71,2.79v1.56h-1.82v2.11h1.82v5.42h-6.7c-0.43,0-0.77-0.35-0.77-0.77V7.78
c0-0.43,0.35-0.77,0.77-0.77h12.44c0.43,0,0.77,0.35,0.77,0.77V20.22z"/>
</svg>

Before

Width:  |  Height:  |  Size: 618 B

-10
View File
@@ -1,10 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="GitHub" fill-rule="evenodd" clip-rule="evenodd" fill="#D1CECC" d="M14.01,0C6.27,0-0.01,6.28-0.01,14.02
c0,6.19,4.02,11.45,9.59,13.3c0.7,0.13,0.96-0.3,0.96-0.68c0-0.33-0.01-1.21-0.02-2.38c-3.9,0.85-4.72-1.88-4.72-1.88
c-0.64-1.62-1.56-2.05-1.56-2.05c-1.27-0.87,0.1-0.85,0.1-0.85c1.41,0.1,2.15,1.44,2.15,1.44c1.25,2.14,3.28,1.52,4.08,1.16
c0.13-0.91,0.49-1.52,0.89-1.87c-3.11-0.35-6.38-1.56-6.38-6.93c0-1.53,0.55-2.78,1.44-3.76C6.37,9.17,5.89,7.74,6.65,5.81
c0,0,1.18-0.38,3.85,1.44c1.12-0.31,2.32-0.47,3.51-0.47c1.19,0.01,2.39,0.16,3.51,0.47c2.68-1.81,3.85-1.44,3.85-1.44
c0.76,1.93,0.28,3.35,0.14,3.71c0.9,0.98,1.44,2.23,1.44,3.76c0,5.38-3.28,6.57-6.4,6.92c0.5,0.43,0.95,1.29,0.95,2.6
c0,1.87-0.02,3.39-0.02,3.84c0,0.37,0.25,0.81,0.96,0.67c5.56-1.86,9.58-7.11,9.58-13.3C28.03,6.28,21.75,0,14.01,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1014 B

-12
View File
@@ -1,12 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="Instagram" fill="#D1CECC" d="M9.89,7.35h0.49v3.05H9.89V7.35z M7.35,8.26v2.15h0.49V7.44C7.55,7.58,7.35,7.89,7.35,8.26z
M18.22,10.3h1.36c0.51,0,0.92-0.41,0.92-0.92V8.38c0-0.51-0.41-0.92-0.92-0.92h-1.36c-0.51,0-0.92,0.41-0.92,0.92v0.99
C17.31,9.87,17.72,10.3,18.22,10.3z M8.2,7.35h0.49v3.05H8.2V7.35z M9.05,7.35h0.49v3.05H9.05V7.35z M14-0.01
C6.26-0.01-0.01,6.27-0.01,14S6.26,28.01,14,28.01S28.01,21.73,28.01,14S21.74-0.01,14-0.01z M6.63,7.93c0-0.71,0.58-1.3,1.3-1.3
h12.14c0.71,0,1.3,0.59,1.3,1.3v3.05h-4.56c-0.64-0.85-1.66-1.39-2.81-1.39s-2.16,0.55-2.81,1.39H6.63V7.93z M17.09,13.1
c0,1.7-1.39,3.09-3.09,3.09s-3.09-1.39-3.09-3.09s1.39-3.09,3.09-3.09C15.7,10.01,17.09,11.39,17.09,13.1z M21.37,20.07
c0,0.71-0.59,1.3-1.3,1.3H7.93c-0.71,0-1.3-0.59-1.3-1.3V11.5h4.24c-0.25,0.48-0.39,1.02-0.39,1.6c0,1.94,1.58,3.52,3.52,3.52
s3.52-1.58,3.52-3.52c0-0.57-0.14-1.12-0.39-1.6h4.24L21.37,20.07L21.37,20.07z M14,10.58c1.39,0,2.52,1.13,2.52,2.52
s-1.13,2.52-2.52,2.52s-2.52-1.13-2.52-2.52S12.61,10.58,14,10.58z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="LinkedIn" fill="#D1CECC" d="M18.82,15.09v3.61h-2.09v-3.37c0-0.85-0.3-1.42-1.06-1.42c-0.58,0-0.92,0.39-1.07,0.77
c-0.06,0.13-0.07,0.32-0.07,0.51v3.52h-2.09c0,0,0.03-5.71,0-6.3h2.09v0.89c0,0.01-0.01,0.01-0.01,0.02h0.01V13.3
c0.28-0.43,0.77-1.04,1.89-1.04C17.79,12.25,18.82,13.16,18.82,15.09z M9.18,18.7h2.09v-6.3H9.18V18.7z M10.24,9.36
c-0.72,0-1.19,0.47-1.19,1.09c0,0.61,0.45,1.09,1.16,1.09h0.01c0.73,0,1.18-0.48,1.18-1.09C11.39,9.84,10.95,9.36,10.24,9.36z
M28,14c0,7.73-6.27,14-14,14S0,21.73,0,14S6.27,0,14,0S28,6.27,28,14z M20.93,8.02c0-0.55-0.46-1-1.02-1H8.09
c-0.57,0-1.02,0.45-1.02,1v11.96c0,0.55,0.46,1,1.02,1h11.82c0.57,0,1.02-0.45,1.02-1V8.02z"/>
</svg>

Before

Width:  |  Height:  |  Size: 867 B

-14
View File
@@ -1,14 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="PDF" fill="#D1CECC" d="M12.28,15.27c0.46-0.94,0.81-1.83,1.05-2.66c0.52,1.29,0.97,2.09,1.35,2.41
c0.38,0.32,0.74,0.53,1.09,0.62c-0.75,0.11-1.45,0.24-2.1,0.38c-0.65,0.14-1.35,0.38-2.1,0.71L12.28,15.27z M28,14
c0,7.73-6.27,14-14,14S0,21.73,0,14S6.27,0,14,0S28,6.27,28,14z M20.94,9.37L17.47,5.9H7.06v16.2h13.89V9.37z M19.35,17.4
c-0.39-0.02-0.86-0.11-1.42-0.27s-1.1-0.36-1.62-0.61c-0.57,0.06-1.14,0.14-1.71,0.25c-0.57,0.11-1.13,0.23-1.67,0.36
c-0.07,0.02-0.24,0.08-0.5,0.17s-0.57,0.21-0.93,0.35c-0.84,1.41-1.52,2.32-2.03,2.74c-0.51,0.42-0.92,0.6-1.22,0.55h11.54v-3.58
c-0.07,0-0.14,0.01-0.21,0.02C19.51,17.39,19.44,17.4,19.35,17.4z M10.17,16.84c0.12-0.22,0.47-0.93,1.05-2.14
c0.58-1.21,1.01-2.24,1.28-3.08c-0.2-0.72-0.33-1.42-0.39-2.09s-0.02-1.23,0.1-1.69c0.07-0.25,0.17-0.44,0.28-0.55
s0.23-0.19,0.35-0.23H8.21v10.85c0.4-0.33,0.78-0.58,1.16-0.75C9.74,16.98,10.01,16.87,10.17,16.84z M13.39,7.06
c0.27,0,0.5,0.1,0.7,0.31c0.2,0.2,0.28,0.54,0.24,1.01c0.01,0.01,0.01,0.3,0,0.87c-0.01,0.57-0.09,1.33-0.24,2.28
c0.52,1.37,1.02,2.27,1.52,2.69c0.49,0.42,0.87,0.67,1.14,0.74c0.52-0.06,0.99-0.09,1.43-0.09c0.31,0,0.61,0.04,0.88,0.13
c0.27,0.08,0.52,0.19,0.73,0.33V9.95l-2.89-2.89H13.39z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-7
View File
@@ -1,7 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="Print" fill="#D1CECC" d="M9.5,7.5h9V9h-9V7.5z M9.5,13.5h9v7h-9V13.5z M10.5,15.51h7v-1h-7V15.51z M10.5,17.49h7v-1h-7
V17.49z M10.5,19.49h7v-1h-7V19.49z M28,14c0,7.73-6.27,14-14,14S0,21.73,0,14S6.27,0,14,0S28,6.27,28,14z M22,11c0-1.1-0.9-2-2-2
h-0.5V6.5h-11V9H8c-1.1,0-2,0.9-2,2v4c0,1.1,0.9,2,2,2h0.5v4.5h11V17H20c1.1,0,2-0.9,2-2V11z M20.02,10.45c-0.28,0-0.5,0.22-0.5,0.5
s0.22,0.5,0.5,0.5s0.5-0.22,0.5-0.5S20.29,10.45,20.02,10.45z"/>
</svg>

Before

Width:  |  Height:  |  Size: 643 B

-10
View File
@@ -1,10 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="Twitter" fill="#D1CECC" d="M14,0C6.27,0,0,6.27,0,14s6.27,14,14,14s14-6.27,14-14S21.73,0,14,0z M20.69,10.57
c0.01,0.15,0.01,0.3,0.01,0.45c0,4.56-3.47,9.82-9.82,9.82c-1.95,0-3.76-0.57-5.29-1.55c0.27,0.03,0.54,0.05,0.82,0.05
c1.62,0,3.11-0.55,4.29-1.48c-1.51-0.03-2.79-1.03-3.23-2.4c0.21,0.04,0.43,0.06,0.65,0.06c0.31,0,0.62-0.04,0.91-0.12
c-1.58-0.32-2.77-1.71-2.77-3.39c0-0.01,0-0.03,0-0.04c0.47,0.26,1,0.41,1.56,0.43c-0.93-0.62-1.54-1.68-1.54-2.87
c0-0.63,0.17-1.23,0.47-1.74c1.7,2.09,4.25,3.46,7.12,3.61c-0.06-0.25-0.09-0.52-0.09-0.79c0-1.91,1.55-3.45,3.45-3.45
c0.99,0,1.89,0.42,2.52,1.09c0.79-0.15,1.53-0.44,2.19-0.84c-0.26,0.81-0.81,1.48-1.52,1.91c0.7-0.08,1.36-0.27,1.98-0.54
C21.95,9.47,21.37,10.08,20.69,10.57z"/>
</svg>

Before

Width:  |  Height:  |  Size: 934 B

-40
View File
@@ -1,40 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve">
<path id="Website" fill="#D1CECC" d="M14.83,7.6c0.03,0.08,0.03,0.14,0,0.19s-0.08,0.07-0.17,0.07c-0.12,0-0.26-0.01-0.4-0.04
s-0.2-0.06-0.18-0.1c0.15-0.15,0.16-0.26,0.03-0.33c-0.13-0.07-0.27-0.11-0.42-0.11s-0.25-0.04-0.3-0.09s-0.03-0.09,0.05-0.09
s0.22,0,0.4,0.01c0.19,0.01,0.31,0.04,0.37,0.08s0.17,0.1,0.33,0.19S14.8,7.52,14.83,7.6z M15.9,13.35c-0.11-0.03-0.2-0.03-0.26,0
s-0.13,0.05-0.2,0.05c-0.08-0.01-0.25-0.09-0.5-0.24s-0.5-0.22-0.77-0.22c-0.11,0.01-0.17,0.04-0.19,0.08s-0.01,0.07,0.01,0.08
c0.02,0.01,0.12,0.03,0.29,0.05s0.4,0.11,0.67,0.26c0.28,0.15,0.48,0.21,0.61,0.19s0.22-0.06,0.28-0.1
c0.07-0.01,0.12-0.03,0.16-0.06S16.01,13.38,15.9,13.35z M13.62,6.85c0.17-0.03,0.3-0.13,0.38-0.3c0.03-0.14-0.04-0.2-0.21-0.18
s-0.29,0.07-0.37,0.13c-0.01,0.03-0.02,0.06-0.03,0.09s-0.02,0.06-0.03,0.07l-0.08,0.1c-0.03,0.03-0.03,0.05-0.02,0.06
C13.32,6.88,13.44,6.89,13.62,6.85z M16.12,7.08c-0.02-0.16-0.11-0.3-0.26-0.41s-0.32-0.19-0.5-0.22c-0.18-0.03-0.38-0.07-0.61-0.12
s-0.4-0.04-0.53,0.04c-0.21,0.18-0.25,0.31-0.12,0.39c0.07,0.03,0.2,0.05,0.38,0.06s0.44,0.15,0.76,0.41
c0.11,0.08,0.29,0.21,0.55,0.37s0.44,0.23,0.55,0.19c0.06-0.06,0.04-0.15-0.05-0.28S16.14,7.24,16.12,7.08z M28,14
c0,7.73-6.27,14-14,14S0,21.73,0,14S6.27,0,14,0S28,6.27,28,14z M21.95,14c-0.06-2.25-0.83-4.12-2.33-5.62
c-1.5-1.5-3.37-2.27-5.62-2.33c-2.25,0.06-4.12,0.83-5.62,2.33c-1.5,1.5-2.27,3.37-2.33,5.62c0.06,2.25,0.83,4.12,2.33,5.62
c1.5,1.5,3.37,2.27,5.62,2.33c0.54,0,1.06-0.05,1.57-0.14c-0.11-0.07-0.17-0.24-0.18-0.51s0.04-0.52,0.16-0.76
c0.15-0.26,0.3-0.61,0.46-1.04c0.15-0.43,0.08-0.77-0.23-1c-0.32-0.18-0.51-0.34-0.57-0.49s-0.19-0.32-0.37-0.54
s-0.28-0.36-0.28-0.44v-0.21c-0.01-0.21,0.05-0.43,0.19-0.66l0.25-0.39c0.01-0.04,0.02-0.1,0.02-0.19v-0.19
c-0.01-0.04-0.07-0.09-0.17-0.16s-0.17-0.1-0.22-0.1s-0.09,0.01-0.11,0.05s-0.07,0.06-0.13,0.06s-0.17-0.04-0.33-0.13
c-0.16-0.09-0.27-0.16-0.32-0.21s-0.1-0.11-0.14-0.19s-0.09-0.15-0.16-0.22s-0.11-0.1-0.16-0.09s-0.12,0-0.23-0.03l-1.45-0.56
c-0.26-0.11-0.43-0.23-0.5-0.37c-0.07-0.14-0.1-0.26-0.1-0.37s-0.06-0.23-0.17-0.37l-0.29-0.37c-0.08-0.12-0.14-0.24-0.17-0.34
s-0.05-0.15-0.06-0.13c-0.01,0.07,0.02,0.23,0.1,0.48s0.12,0.39,0.1,0.43c-0.03,0.01-0.07-0.02-0.12-0.11s-0.12-0.19-0.19-0.3
c-0.06-0.07-0.07-0.15-0.04-0.25c0.03-0.1-0.07-0.38-0.29-0.85c-0.22-0.47-0.26-0.85-0.11-1.15s0.24-0.57,0.3-0.82
c0.03-0.21,0.09-0.25,0.19-0.13c0.1,0.12,0.1,0.11,0.02-0.01c-0.08-0.18-0.11-0.4-0.08-0.65c0.03-0.26,0.01-0.46-0.06-0.61
c-0.11-0.1-0.3-0.08-0.58,0.05S9.12,8.14,9.1,8.16c0.03-0.12,0.18-0.28,0.46-0.46l0.87-0.58c0.26-0.17,0.49-0.22,0.69-0.17
s0.41,0.12,0.62,0.21s0.34,0.12,0.37,0.1s0.01-0.06-0.06-0.13s-0.07-0.13,0.01-0.16S12.26,6.96,12.43,7
c0.17,0.03,0.31,0.11,0.42,0.24s0.3,0.18,0.54,0.17s0.32,0.01,0.25,0.06s-0.11,0.12-0.11,0.19c0.04,0.06,0.02,0.1-0.05,0.13
s-0.2,0.1-0.36,0.21s-0.18,0.19-0.05,0.26c0.13,0.07,0.36,0.21,0.67,0.41c0.28,0.19,0.41,0.19,0.41-0.01s-0.02-0.4-0.06-0.61
c-0.01-0.14,0.05-0.19,0.2-0.14s0.22,0.07,0.24,0.08c0.17,0.11,0.27,0.15,0.32,0.11s0.13-0.04,0.26-0.01
c0.17,0.07,0.37,0.19,0.6,0.37l0.41,0.33c-0.43,0.25-0.6,0.41-0.51,0.49s0.2,0.14,0.32,0.2c0.06,0.06,0.02,0.12-0.1,0.21
s-0.2,0.12-0.23,0.12c-0.08-0.08-0.15-0.1-0.2-0.05s-0.1,0.09-0.16,0.13c-0.06,0.04-0.07,0.09-0.05,0.14s0.04,0.09,0.05,0.1
c-0.3,0.07-0.47,0.22-0.49,0.44s-0.02,0.39-0.01,0.49c0,0.08-0.07,0.18-0.21,0.3s-0.24,0.23-0.31,0.34c-0.03,0.14,0,0.29,0.08,0.47
c0.08,0.17,0.08,0.27,0,0.3c-0.08-0.01-0.22-0.12-0.41-0.32c-0.19-0.2-0.55-0.24-1.08-0.13c-0.19,0.04-0.39,0.16-0.6,0.36
s-0.23,0.49-0.06,0.88c0.21,0.32,0.43,0.38,0.68,0.19c0.25-0.19,0.43-0.3,0.56-0.31c0.1,0.08,0.12,0.2,0.06,0.35
c-0.06,0.15-0.08,0.24-0.08,0.27c0.06-0.01,0.17,0.02,0.34,0.09s0.28,0.27,0.32,0.58c0.04,0.31,0.19,0.49,0.46,0.55
s0.45,0.08,0.57,0.07s0.28-0.09,0.48-0.24s0.32-0.24,0.37-0.27s0.17-0.06,0.35-0.1s0.47,0.03,0.84,0.23s0.67,0.31,0.88,0.35
s0.38,0.11,0.5,0.2s0.18,0.2,0.18,0.33s0.05,0.23,0.14,0.3c0.15,0.04,0.39,0.07,0.72,0.07s0.57,0.1,0.72,0.28
c0.07,0.28-0.08,0.73-0.46,1.38c-0.37,0.64-0.68,1.05-0.93,1.23c-0.18,0.12-0.34,0.29-0.48,0.49s-0.3,0.38-0.49,0.55
s-0.41,0.34-0.66,0.52s-0.51,0.34-0.78,0.5c-0.21,0.15-0.36,0.32-0.46,0.51s-0.19,0.32-0.27,0.4c1.81-0.43,3.28-1.34,4.43-2.75
C21.31,17.6,21.9,15.93,21.95,14z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.
-66
View File
@@ -1,66 +0,0 @@
# Site settings
title: "Pengzhan.dev"
description: "Pengzhan Hao's personal biography"
# Build settings
markdown: kramdown
sass:
sass_dir: _sass
style: compressed
# Resume settings
resume_avatar: "true"
resume_name: "Pengzhan Hao"
resume_title: "Software Engineer"
# used for the contact button in _layouts/resume.html
resume_contact_email: "[email protected]"
# the next two items are used for schema itemprops in _layouts/resume.html
#resume_contact_telephone: "555-7334"
#resume_contact_address: "742 Evergreen Terrace, Springfield"
#resume_header_contact_info: "742 Evergreen Terrace, Springfield • 555-7334 • [email protected]"
#display_header_contact_info: "false"
resume_header_intro: "<p>Software engineer on Cloud, research focus on Edge computing and Distributed systems</p>"
# use "yes" to display the email contact button,
# "no" to display an "I'm not looking for work" message,
# or remove the resume_looking_for_work option entirely
# to leave blank
resume_looking_for_work: "no"
# Decide which sections to use
# comment out to hide
resume_section_experience: true
resume_section_education: true
resume_section_publications: true
resume_section_projects: true
resume_section_skills: true
resume_section_recognition: true
resume_section_links: true
# resume_section_associations: true
# resume_section_interests: true
# Resume social links
# uncomment the options you wish to display, and add your own URL
resume_social_links:
resume_github_url: "https://github.com/CoderSherlock"
#resume_twitter_url: "http://twitter.com/jglovier"
#resume_dribbble_url: "https://dribbble.com/jag"
# resume_facebook_url: "insert Facebook URL here"
resume_linkedin_url: "https://www.linkedin.com/in/pengzhanhao/"
resume_instagram_url: "https://www.instagram.com/pengzhan.hao/"
resume_website_url: "http://pengzhan.dev"
#resume_medium_url: "https://medium.com/@jglovier"
resume_print_social_links: true
# Design settings
resume_theme: default
# Note: when editing locally, remember to restart
# your Jekyll server when you edit this file. Changes
# to _config.yml are only recognized when the server starts
exclude: ['README.md', 'LICENSE', 'Dockerfile', 'CNAME', 'Gemfile', 'Gemfile.lock', 'deploy.sh']
-15
View File
@@ -1,15 +0,0 @@
# Associations
# comment out the urls if you do not wish to display them
- organization: PETA
role: Volunteer
year: 2001 &mdash; 2010
url: https://www.peta.org/
summary: Organized fund drives and participated in fundraising events for the benefit of PETA's local Springfield operations.
- organization: Yale Amnesty International
role: Communications coordinator
year: 2002 &mdash; 2004
url: http://campuspress.yale.edu/amnesty/
summary: During my junion and senior years at Yale, I was actively involved in Yale Amnesty International where I helped facilitate our student organization communications through website maintenance, writing for our campus publication, and graphic design of related materials.
-14
View File
@@ -1,14 +0,0 @@
# Degree
- degree: Doctorate of Science in Computer Science
uni: Binghamton university, the State University of New York
year: 2017 &mdash;
- degree: Master of Science in Computer Science
uni: Binghamton university, the State University of New York
year: 2015 &mdash; 2017
summary: GPA 4.0/4.0
- degree: Bachelor of Engineering in Software Engineering
uni: Beihang University
year: 2011 &mdash; 2015
summary: GPA 3.25/4.0
-34
View File
@@ -1,34 +0,0 @@
# Jobs
# Google
- company: Google
position: Software Engineer
duration: Jun, 2022 &mdash;
summary: Keep GKE nodes working
# Graduate position
- company: Binghamton University, Mobile and Operating System Lab
position: Research Assistant, advised by Prof. Yifan Zhang
duration: Oct, 2016 &mdash;
summary: Edge-based distributed deep learning
<ul class="resume-item-list">
<li>Implemented a system for time-sensitive, edge-based distributed neural network training scenarios with state-of-the-art performanc</li>
<li>Applied empirical experiments to show potential opportunities in Edge-based/Mobile-based distributed neural network training.</li>
<li>Investigated heterogeneous-aware training tasks scheduling problem. </li>
</ul>
Edge computing
<ul class="resume-item-list">
<li>Envision edge computing's work-flow in end device performance(latency, traffic) improving.</li>
<li>Applied empirical experiments on different edge computing work-flow and propose/implement related solutions. </li>
<li>Design and implement an edge-based multi-tier system for decreasing bandwidth usage from endpoint mobile device. </li>
</ul>
# Undergrad
- company: Advance computer application technology engineering research center
position: Research Assistant, advised by Prof. Juhua Pu
duration: Oct, 2013 &mdash; Sep, 2014
summary: Real-time routing
<ul class="resume-item-list">
<li>Studied on real-time routing problem based vehicle location data </li>
<li>Optimized current routing algorithm with predicted traffic information </li>
</ul>
-4
View File
@@ -1,4 +0,0 @@
# Interests
- description: Human rights and world affairs
- description: Music (baritone saxiphonist)
- description: Linguistics
-3
View File
@@ -1,3 +0,0 @@
# links
- url: "https://blog.pengzhan.dev/"
description: My personal blog
-32
View File
@@ -1,32 +0,0 @@
# Projects
# uncomment the urls if you wish to display them, and add your own URL
- project: EDDL - Edge distributed deep learning framework
role: Leader
duration: 2018 &mdash; Present
# url: "#"
description: A lite-weight c++ written framework(based on Dlib) for distributed neural network training on embedded devices and
mobile phones. With supporting of our implemented runtime profilers, training devices can interconnect in an optimized
network topology under various network environments.
In our last publication, we showed our framework can perform distributed training for diverse neural networks on
resource-limited devices. The EDDL framework also showed well scalability in large amounts of devices.
We are currently working on network optimization on per-batch communication time and overall convergence time.
- project: Edge Courier
role: Leader
duration: 2016 &mdash; 2017
# url: "#"
description: A proxy-based protocol for incremental file synchronization and cooperative online document editing. This protocol
supports file hosting services including Google Drive, Dropbox, and OneDrive as well as document collaboration services
such as G Suite and Microsoft 365.
Edge courier can significantly decrease bandwidth consumption for collaborative editing web apps, and also showed
advancement in cloud file storage services.
- project: RTRouting assistant
role: Undergraduate-level student researcher
duration: 2013 &mdash; 2014
# url: "#"
description: A real time routing prototype which can guide vehicle to alternative routes with high-volume traffic prediction. The
predicted traffic alert is generated by a pre-trained model learned from history monitored traffics, real time event
warnings and a abnormal traffic prediction model. This project belongs to a urban computing project studied together
by Advance computer application technology engineering research center(Beihang University) and Microsoft Research
Asia(MSRA).
-29
View File
@@ -1,29 +0,0 @@
# Publications
# uncomment the urls if you wish to display them, and add your own URL
- name: "EDDL: A Distributed Deep Learning System for Resource-limited Edge Computing Environment"
url: "https://blog.pengzhan.dev/static/2021-12/eddl-sec21.pdf"
description: "Pengzhan Hao, and Yifan Zhang The Sixth ACM/IEEE Symposium on Edge Computing (ACM/IEEE SEC)
San Jose, CA, USA, 2021."
- name: A Case for Web Service Bandwidth Reduction on Mobile Devices with Edge-hosted Personal Services
description: "Yongshu Bai, Pengzhan Hao, and Yifan Zhang The 37th IEEE International ConferenDDL - Edge distributed deep learning frameworkce on Computer Communications (IEEE INFOCOM)
Honolulu, HI, USA, 2018."
url: "https://blog.pengzhan.dev/static/2021-10/eta-infocom18.pdf"
- name: "EdgeCourier: An Edge-hosted Personal Service for Low-bandwidth Document Synchronization in Mobile Cloud Storage Services"
description: "Pengzhan Hao, Yongshu Bai, Xin Zhang, and Yifan Zhang
The 2nd ACM/IEEE Symposium on Edge Computing (ACM/IEEE SEC)
San Jose, CA, USA, 2017."
url: "https://blog.pengzhan.dev/static/2018-02/a7-hao.pdf"
- name: "Poster: Securing Device Inputs for Smartphones Using Hypervisor Based Approach"
description: "Xin Zhang, Yongshu Bai, Pengzhan Hao, and Yifan Zhang
The 15th ACM International Conference on Mobile Systems, Applications, and Services (ACM MobiSys)
Niagara Falls, NY, USA, 2017."
url: "https://blog.pengzhan.dev/static/2017-07/p169-zhang.pdf"
- name: "Poster: EPS - Edge-hosted Personal Services for Mobile Users"
description: "Pengzhan Hao, Yongshu Bai, Xin Zhang, and Yifan Zhang
The 15th ACM International Conference on Mobile Systems, Applications, and Services (ACM MobiSys)
Niagara Falls, NY, USA, 2017."
url: "https://blog.pengzhan.dev/static/2017-07/p163-hao.pdf"
-8
View File
@@ -1,8 +0,0 @@
# Recognitions
- award: ACM/IEEE SEC student travel grant
organization: IEEE
year: 2017, 2021
- award: ACM MobiSys 2017 student travel grant
organization: ACM
year: 2017
-4
View File
@@ -1,4 +0,0 @@
# Skills
# Organizational leadership
- skill: Professional skills
description: "C/C++, Python, Golang, Java, Bash, SQL, Docker, Kubernetes, Xen hypervisor, RESTful, Django, OAuth, ProtoBuf"
-27
View File
@@ -1,27 +0,0 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}</title>
<!-- Google font typography settings - defined in _config.yml -->
{% if site.resume_theme == 'default' %}
<link href='https://fonts.googleapis.com/css?family=Lora:400,700|Open+Sans:400,300,800,700' rel='stylesheet' type='text/css'>
{% endif %}
<meta name="description" content="{% if page.excerpt %}{{ page.excerpt | strip_html | strip_newlines | truncate: 160 }}{% else %}{{ site.description }}{% endif %}">
<link rel="stylesheet" href="css/main.css">
<link rel="canonical" href="{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}">
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-MNP4QN1T9T"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-MNP4QN1T9T');
</script>
</head>
-48
View File
@@ -1,48 +0,0 @@
<!-- and guess where these are defined? Yup, you guessed it: the _config.yml file -->
{% if site.resume_social_links %}
<ul class="icon-links">
<!-- GitHub link -->
{% if site.resume_social_links.resume_github_url %}
<li class="icon-link-item"><a href="{{ site.resume_social_links.resume_github_url }}" class="icon-link" itemprop="sameAs">{% include icons/icon-github.html %}</a></li>
{% endif %}
<!-- Twitter link -->
{% if site.resume_social_links.resume_twitter_url %}
<li class="icon-link-item"><a href="{{ site.resume_social_links.resume_twitter_url }}" class="icon-link" itemprop="sameAs">{% include icons/icon-twitter.html %}</a></li>
{% endif %}
<!-- Medium link -->
{% if site.resume_social_links.resume_medium_url %}
<li class="icon-link-item">
<a href="{{ site.resume_social_links.resume_medium_url }}" class="icon-link" itemprop="url">{% include icons/icon-medium.html %}</a>
</li>
{% endif %}
<!-- Dribbble link -->
{% if site.resume_social_links.resume_dribbble_url %}
<li class="icon-link-item"><a href="{{ site.resume_social_links.resume_dribbble_url }}" class="icon-link" itemprop="sameAs">{% include icons/icon-dribbble.html %}</a></li>
{% endif %}
<!-- Facebook link -->
{% if site.resume_social_links.resume_facebook_url %}
<li class="icon-link-item"><a href="{{ site.resume_social_links.resume_facebook_url }}" class="icon-link" itemprop="sameAs">{% include icons/icon-facebook.html %}</a></li>
{% endif %}
<!-- LinkedIn link -->
{% if site.resume_social_links.resume_linkedin_url %}
<li class="icon-link-item"><a href="{{ site.resume_social_links.resume_linkedin_url }}" class="icon-link" itemprop="sameAs">{% include icons/icon-linkedin.html %}</a></li>
{% endif %}
<!-- Instagram link -->
{% if site.resume_social_links.resume_instagram_url %}
<li class="icon-link-item"><a href="{{ site.resume_social_links.resume_instagram_url }}" class="icon-link" itemprop="sameAs">{% include icons/icon-instagram.html %}</a></li>
{% endif %}
<!-- Website link -->
{% if site.resume_social_links.resume_website_url %}
<li class="icon-link-item"><a href="{{ site.resume_social_links.resume_website_url }}" class="icon-link" itemprop="url">{% include icons/icon-website.html %}</a></li>
{% endif %}
</ul>
{% endif %}
-17
View File
@@ -1,17 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="Dribbble" fill="#D1CECC" d="M13.76,14.4c0.37-0.15,0.78-0.26,1.16-0.43c-0.18-0.34-0.35-0.73-0.5-1.04
c-0.06-0.11-0.1-0.25-0.2-0.33c-2.49,0.72-5.25,1.17-8.48,1.16c-0.06,2.6,0.88,4.41,2.12,5.79C9.3,17.33,11.22,15.46,13.76,14.4z
M15.45,15.38c-2.87,1.04-5.13,2.68-6.53,5.19c0.95,0.74,2.22,1.38,3.72,1.65c1.65,0.29,3.38-0.01,4.6-0.55
c-0.37-2.12-0.97-4.14-1.65-5.92C15.57,15.62,15.57,15.46,15.45,15.38z M10.47,6.52c-2.3,1.15-3.92,2.99-4.58,5.79
c2.87-0.04,5.42-0.39,7.66-1.04C12.58,9.64,11.6,8.01,10.47,6.52z M14.01,0C6.27,0,0,6.28,0,14.01s6.27,14.01,14.01,14.01
c7.05,0,12.89-5.21,13.86-12c0.1-0.46,0.14-0.94,0.13-1.43c0.01-0.2,0.01-0.39,0.01-0.58C28.02,6.28,21.75,0,14.01,0z M21.17,20.6
c-0.77,0.85-1.69,1.53-2.78,2.07c-1.09,0.55-2.33,0.97-3.8,1.04c-3.1,0.15-5.52-1.04-7.14-2.53c-1.66-1.53-2.92-3.59-3.14-6.56
c-0.11-1.58,0.2-2.97,0.67-4.15c0.48-1.23,1.06-2.14,1.84-3.01c1.11-1.2,2.61-2.26,4.5-2.78c0.68-0.19,1.39-0.31,2.1-0.36
c3.17-0.2,5.51,1.07,7.16,2.56c1.65,1.48,2.94,3.62,3.11,6.58C23.89,16.57,22.7,18.94,21.17,20.6z M17.05,14.98
c0.64,1.86,1.23,3.76,1.59,5.9c0.46-0.26,0.86-0.61,1.24-0.99c1.14-1.15,1.99-2.63,2.33-4.55C20.79,14.91,18.79,14.7,17.05,14.98z
M15.2,10.8c1.73-0.7,3.28-1.6,4.3-3.01c-1.36-1.14-3.32-2.18-5.82-2.05c-0.6,0.03-1.05,0.08-1.59,0.2
c0.93,1.37,1.93,2.79,2.73,4.22C14.94,10.38,15.02,10.63,15.2,10.8z M15.81,12.06c0.2,0.38,0.39,0.81,0.56,1.19
c0.06,0.14,0.08,0.31,0.2,0.41c1.93-0.22,4.06-0.06,5.74,0.28c-0.06-2.27-0.82-3.85-1.87-5.14C19.24,10.23,17.65,11.27,15.81,12.06z
"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-7
View File
@@ -1,7 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="Facebook" fill="#D1CECC" d="M14,0C6.27,0,0,6.27,0,14s6.27,14,14,14s14-6.27,14-14S21.73,0,14,0z M20.99,20.22
c0,0.43-0.35,0.77-0.77,0.77h-3.56v-5.42h1.82l0.27-2.11h-2.09v-1.35c0-0.61,0.17-1.03,1.05-1.03l1.12,0V9.2
c-0.19-0.03-0.86-0.08-1.63-0.08c-1.61,0-2.71,0.98-2.71,2.79v1.56h-1.82v2.11h1.82v5.42h-6.7c-0.43,0-0.77-0.35-0.77-0.77V7.78
c0-0.43,0.35-0.77,0.77-0.77h12.44c0.43,0,0.77,0.35,0.77,0.77V20.22z"/>
</svg>

Before

Width:  |  Height:  |  Size: 633 B

-10
View File
@@ -1,10 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="GitHub" fill-rule="evenodd" clip-rule="evenodd" fill="#D1CECC" d="M14.01,0C6.27,0-0.01,6.28-0.01,14.02
c0,6.19,4.02,11.45,9.59,13.3c0.7,0.13,0.96-0.3,0.96-0.68c0-0.33-0.01-1.21-0.02-2.38c-3.9,0.85-4.72-1.88-4.72-1.88
c-0.64-1.62-1.56-2.05-1.56-2.05c-1.27-0.87,0.1-0.85,0.1-0.85c1.41,0.1,2.15,1.44,2.15,1.44c1.25,2.14,3.28,1.52,4.08,1.16
c0.13-0.91,0.49-1.52,0.89-1.87c-3.11-0.35-6.38-1.56-6.38-6.93c0-1.53,0.55-2.78,1.44-3.76C6.37,9.17,5.89,7.74,6.65,5.81
c0,0,1.18-0.38,3.85,1.44c1.12-0.31,2.32-0.47,3.51-0.47c1.19,0.01,2.39,0.16,3.51,0.47c2.68-1.81,3.85-1.44,3.85-1.44
c0.76,1.93,0.28,3.35,0.14,3.71c0.9,0.98,1.44,2.23,1.44,3.76c0,5.38-3.28,6.57-6.4,6.92c0.5,0.43,0.95,1.29,0.95,2.6
c0,1.87-0.02,3.39-0.02,3.84c0,0.37,0.25,0.81,0.96,0.67c5.56-1.86,9.58-7.11,9.58-13.3C28.03,6.28,21.75,0,14.01,0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-12
View File
@@ -1,12 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="Instagram" fill="#D1CECC" d="M9.89,7.35h0.49v3.05H9.89V7.35z M7.35,8.26v2.15h0.49V7.44C7.55,7.58,7.35,7.89,7.35,8.26z
M18.22,10.3h1.36c0.51,0,0.92-0.41,0.92-0.92V8.38c0-0.51-0.41-0.92-0.92-0.92h-1.36c-0.51,0-0.92,0.41-0.92,0.92v0.99
C17.31,9.87,17.72,10.3,18.22,10.3z M8.2,7.35h0.49v3.05H8.2V7.35z M9.05,7.35h0.49v3.05H9.05V7.35z M14-0.01
C6.26-0.01-0.01,6.27-0.01,14S6.26,28.01,14,28.01S28.01,21.73,28.01,14S21.74-0.01,14-0.01z M6.63,7.93c0-0.71,0.58-1.3,1.3-1.3
h12.14c0.71,0,1.3,0.59,1.3,1.3v3.05h-4.56c-0.64-0.85-1.66-1.39-2.81-1.39s-2.16,0.55-2.81,1.39H6.63V7.93z M17.09,13.1
c0,1.7-1.39,3.09-3.09,3.09s-3.09-1.39-3.09-3.09s1.39-3.09,3.09-3.09C15.7,10.01,17.09,11.39,17.09,13.1z M21.37,20.07
c0,0.71-0.59,1.3-1.3,1.3H7.93c-0.71,0-1.3-0.59-1.3-1.3V11.5h4.24c-0.25,0.48-0.39,1.02-0.39,1.6c0,1.94,1.58,3.52,3.52,3.52
s3.52-1.58,3.52-3.52c0-0.57-0.14-1.12-0.39-1.6h4.24L21.37,20.07L21.37,20.07z M14,10.58c1.39,0,2.52,1.13,2.52,2.52
s-1.13,2.52-2.52,2.52s-2.52-1.13-2.52-2.52S12.61,10.58,14,10.58z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="LinkedIn" fill="#D1CECC" d="M18.82,15.09v3.61h-2.09v-3.37c0-0.85-0.3-1.42-1.06-1.42c-0.58,0-0.92,0.39-1.07,0.77
c-0.06,0.13-0.07,0.32-0.07,0.51v3.52h-2.09c0,0,0.03-5.71,0-6.3h2.09v0.89c0,0.01-0.01,0.01-0.01,0.02h0.01V13.3
c0.28-0.43,0.77-1.04,1.89-1.04C17.79,12.25,18.82,13.16,18.82,15.09z M9.18,18.7h2.09v-6.3H9.18V18.7z M10.24,9.36
c-0.72,0-1.19,0.47-1.19,1.09c0,0.61,0.45,1.09,1.16,1.09h0.01c0.73,0,1.18-0.48,1.18-1.09C11.39,9.84,10.95,9.36,10.24,9.36z
M28,14c0,7.73-6.27,14-14,14S0,21.73,0,14S6.27,0,14,0S28,6.27,28,14z M20.93,8.02c0-0.55-0.46-1-1.02-1H8.09
c-0.57,0-1.02,0.45-1.02,1v11.96c0,0.55,0.46,1,1.02,1h11.82c0.57,0,1.02-0.45,1.02-1V8.02z"/>
</svg>

Before

Width:  |  Height:  |  Size: 884 B

-6
View File
@@ -1,6 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="Medium" fill="#D1CECC" d="M14,0C6.3,0,0,6.3,0,14s6.3,14,14,14s14-6.3,14-14S21.7,0,14,0z M11.2,19.5H7v-0.2l1.5-1.8
c0.2-0.2,0.2-0.4,0.2-0.6v-6.2c0-0.2,0-0.3-0.2-0.5L7.2,8.7V8.4h4.1l3.1,6.9l2.8-6.9H21v0.2l-1.1,1.1c-0.1,0.1-0.1,0.2-0.1,0.3v7.9
c0,0.1,0,0.2,0.1,0.3l1.1,1.1v0.2h-5.5v-0.2l1.1-1.1c0.1-0.1,0.1-0.1,0.1-0.3v-6.4l-3.1,8h-0.4l-3.7-8v5.4c0,0.2,0,0.5,0.2,0.6
l1.5,1.8C11.2,19.3,11.2,19.5,11.2,19.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 627 B

-14
View File
@@ -1,14 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="PDF" fill="#D1CECC" d="M12.28,15.27c0.46-0.94,0.81-1.83,1.05-2.66c0.52,1.29,0.97,2.09,1.35,2.41
c0.38,0.32,0.74,0.53,1.09,0.62c-0.75,0.11-1.45,0.24-2.1,0.38c-0.65,0.14-1.35,0.38-2.1,0.71L12.28,15.27z M28,14
c0,7.73-6.27,14-14,14S0,21.73,0,14S6.27,0,14,0S28,6.27,28,14z M20.94,9.37L17.47,5.9H7.06v16.2h13.89V9.37z M19.35,17.4
c-0.39-0.02-0.86-0.11-1.42-0.27s-1.1-0.36-1.62-0.61c-0.57,0.06-1.14,0.14-1.71,0.25c-0.57,0.11-1.13,0.23-1.67,0.36
c-0.07,0.02-0.24,0.08-0.5,0.17s-0.57,0.21-0.93,0.35c-0.84,1.41-1.52,2.32-2.03,2.74c-0.51,0.42-0.92,0.6-1.22,0.55h11.54v-3.58
c-0.07,0-0.14,0.01-0.21,0.02C19.51,17.39,19.44,17.4,19.35,17.4z M10.17,16.84c0.12-0.22,0.47-0.93,1.05-2.14
c0.58-1.21,1.01-2.24,1.28-3.08c-0.2-0.72-0.33-1.42-0.39-2.09s-0.02-1.23,0.1-1.69c0.07-0.25,0.17-0.44,0.28-0.55
s0.23-0.19,0.35-0.23H8.21v10.85c0.4-0.33,0.78-0.58,1.16-0.75C9.74,16.98,10.01,16.87,10.17,16.84z M13.39,7.06
c0.27,0,0.5,0.1,0.7,0.31c0.2,0.2,0.28,0.54,0.24,1.01c0.01,0.01,0.01,0.3,0,0.87c-0.01,0.57-0.09,1.33-0.24,2.28
c0.52,1.37,1.02,2.27,1.52,2.69c0.49,0.42,0.87,0.67,1.14,0.74c0.52-0.06,0.99-0.09,1.43-0.09c0.31,0,0.61,0.04,0.88,0.13
c0.27,0.08,0.52,0.19,0.73,0.33V9.95l-2.89-2.89H13.39z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-7
View File
@@ -1,7 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="Print" fill="#D1CECC" d="M9.5,7.5h9V9h-9V7.5z M9.5,13.5h9v7h-9V13.5z M10.5,15.51h7v-1h-7V15.51z M10.5,17.49h7v-1h-7
V17.49z M10.5,19.49h7v-1h-7V19.49z M28,14c0,7.73-6.27,14-14,14S0,21.73,0,14S6.27,0,14,0S28,6.27,28,14z M22,11c0-1.1-0.9-2-2-2
h-0.5V6.5h-11V9H8c-1.1,0-2,0.9-2,2v4c0,1.1,0.9,2,2,2h0.5v4.5h11V17H20c1.1,0,2-0.9,2-2V11z M20.02,10.45c-0.28,0-0.5,0.22-0.5,0.5
s0.22,0.5,0.5,0.5s0.5-0.22,0.5-0.5S20.29,10.45,20.02,10.45z"/>
</svg>

Before

Width:  |  Height:  |  Size: 658 B

-10
View File
@@ -1,10 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="Twitter" fill="#D1CECC" d="M14,0C6.27,0,0,6.27,0,14s6.27,14,14,14s14-6.27,14-14S21.73,0,14,0z M20.69,10.57
c0.01,0.15,0.01,0.3,0.01,0.45c0,4.56-3.47,9.82-9.82,9.82c-1.95,0-3.76-0.57-5.29-1.55c0.27,0.03,0.54,0.05,0.82,0.05
c1.62,0,3.11-0.55,4.29-1.48c-1.51-0.03-2.79-1.03-3.23-2.4c0.21,0.04,0.43,0.06,0.65,0.06c0.31,0,0.62-0.04,0.91-0.12
c-1.58-0.32-2.77-1.71-2.77-3.39c0-0.01,0-0.03,0-0.04c0.47,0.26,1,0.41,1.56,0.43c-0.93-0.62-1.54-1.68-1.54-2.87
c0-0.63,0.17-1.23,0.47-1.74c1.7,2.09,4.25,3.46,7.12,3.61c-0.06-0.25-0.09-0.52-0.09-0.79c0-1.91,1.55-3.45,3.45-3.45
c0.99,0,1.89,0.42,2.52,1.09c0.79-0.15,1.53-0.44,2.19-0.84c-0.26,0.81-0.81,1.48-1.52,1.91c0.7-0.08,1.36-0.27,1.98-0.54
C21.95,9.47,21.37,10.08,20.69,10.57z"/>
</svg>

Before

Width:  |  Height:  |  Size: 952 B

-40
View File
@@ -1,40 +0,0 @@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 28 28" enable-background="new 0 0 28 28" xml:space="preserve" width="28">
<path id="Website" fill="#D1CECC" d="M14.83,7.6c0.03,0.08,0.03,0.14,0,0.19s-0.08,0.07-0.17,0.07c-0.12,0-0.26-0.01-0.4-0.04
s-0.2-0.06-0.18-0.1c0.15-0.15,0.16-0.26,0.03-0.33c-0.13-0.07-0.27-0.11-0.42-0.11s-0.25-0.04-0.3-0.09s-0.03-0.09,0.05-0.09
s0.22,0,0.4,0.01c0.19,0.01,0.31,0.04,0.37,0.08s0.17,0.1,0.33,0.19S14.8,7.52,14.83,7.6z M15.9,13.35c-0.11-0.03-0.2-0.03-0.26,0
s-0.13,0.05-0.2,0.05c-0.08-0.01-0.25-0.09-0.5-0.24s-0.5-0.22-0.77-0.22c-0.11,0.01-0.17,0.04-0.19,0.08s-0.01,0.07,0.01,0.08
c0.02,0.01,0.12,0.03,0.29,0.05s0.4,0.11,0.67,0.26c0.28,0.15,0.48,0.21,0.61,0.19s0.22-0.06,0.28-0.1
c0.07-0.01,0.12-0.03,0.16-0.06S16.01,13.38,15.9,13.35z M13.62,6.85c0.17-0.03,0.3-0.13,0.38-0.3c0.03-0.14-0.04-0.2-0.21-0.18
s-0.29,0.07-0.37,0.13c-0.01,0.03-0.02,0.06-0.03,0.09s-0.02,0.06-0.03,0.07l-0.08,0.1c-0.03,0.03-0.03,0.05-0.02,0.06
C13.32,6.88,13.44,6.89,13.62,6.85z M16.12,7.08c-0.02-0.16-0.11-0.3-0.26-0.41s-0.32-0.19-0.5-0.22c-0.18-0.03-0.38-0.07-0.61-0.12
s-0.4-0.04-0.53,0.04c-0.21,0.18-0.25,0.31-0.12,0.39c0.07,0.03,0.2,0.05,0.38,0.06s0.44,0.15,0.76,0.41
c0.11,0.08,0.29,0.21,0.55,0.37s0.44,0.23,0.55,0.19c0.06-0.06,0.04-0.15-0.05-0.28S16.14,7.24,16.12,7.08z M28,14
c0,7.73-6.27,14-14,14S0,21.73,0,14S6.27,0,14,0S28,6.27,28,14z M21.95,14c-0.06-2.25-0.83-4.12-2.33-5.62
c-1.5-1.5-3.37-2.27-5.62-2.33c-2.25,0.06-4.12,0.83-5.62,2.33c-1.5,1.5-2.27,3.37-2.33,5.62c0.06,2.25,0.83,4.12,2.33,5.62
c1.5,1.5,3.37,2.27,5.62,2.33c0.54,0,1.06-0.05,1.57-0.14c-0.11-0.07-0.17-0.24-0.18-0.51s0.04-0.52,0.16-0.76
c0.15-0.26,0.3-0.61,0.46-1.04c0.15-0.43,0.08-0.77-0.23-1c-0.32-0.18-0.51-0.34-0.57-0.49s-0.19-0.32-0.37-0.54
s-0.28-0.36-0.28-0.44v-0.21c-0.01-0.21,0.05-0.43,0.19-0.66l0.25-0.39c0.01-0.04,0.02-0.1,0.02-0.19v-0.19
c-0.01-0.04-0.07-0.09-0.17-0.16s-0.17-0.1-0.22-0.1s-0.09,0.01-0.11,0.05s-0.07,0.06-0.13,0.06s-0.17-0.04-0.33-0.13
c-0.16-0.09-0.27-0.16-0.32-0.21s-0.1-0.11-0.14-0.19s-0.09-0.15-0.16-0.22s-0.11-0.1-0.16-0.09s-0.12,0-0.23-0.03l-1.45-0.56
c-0.26-0.11-0.43-0.23-0.5-0.37c-0.07-0.14-0.1-0.26-0.1-0.37s-0.06-0.23-0.17-0.37l-0.29-0.37c-0.08-0.12-0.14-0.24-0.17-0.34
s-0.05-0.15-0.06-0.13c-0.01,0.07,0.02,0.23,0.1,0.48s0.12,0.39,0.1,0.43c-0.03,0.01-0.07-0.02-0.12-0.11s-0.12-0.19-0.19-0.3
c-0.06-0.07-0.07-0.15-0.04-0.25c0.03-0.1-0.07-0.38-0.29-0.85c-0.22-0.47-0.26-0.85-0.11-1.15s0.24-0.57,0.3-0.82
c0.03-0.21,0.09-0.25,0.19-0.13c0.1,0.12,0.1,0.11,0.02-0.01c-0.08-0.18-0.11-0.4-0.08-0.65c0.03-0.26,0.01-0.46-0.06-0.61
c-0.11-0.1-0.3-0.08-0.58,0.05S9.12,8.14,9.1,8.16c0.03-0.12,0.18-0.28,0.46-0.46l0.87-0.58c0.26-0.17,0.49-0.22,0.69-0.17
s0.41,0.12,0.62,0.21s0.34,0.12,0.37,0.1s0.01-0.06-0.06-0.13s-0.07-0.13,0.01-0.16S12.26,6.96,12.43,7
c0.17,0.03,0.31,0.11,0.42,0.24s0.3,0.18,0.54,0.17s0.32,0.01,0.25,0.06s-0.11,0.12-0.11,0.19c0.04,0.06,0.02,0.1-0.05,0.13
s-0.2,0.1-0.36,0.21s-0.18,0.19-0.05,0.26c0.13,0.07,0.36,0.21,0.67,0.41c0.28,0.19,0.41,0.19,0.41-0.01s-0.02-0.4-0.06-0.61
c-0.01-0.14,0.05-0.19,0.2-0.14s0.22,0.07,0.24,0.08c0.17,0.11,0.27,0.15,0.32,0.11s0.13-0.04,0.26-0.01
c0.17,0.07,0.37,0.19,0.6,0.37l0.41,0.33c-0.43,0.25-0.6,0.41-0.51,0.49s0.2,0.14,0.32,0.2c0.06,0.06,0.02,0.12-0.1,0.21
s-0.2,0.12-0.23,0.12c-0.08-0.08-0.15-0.1-0.2-0.05s-0.1,0.09-0.16,0.13c-0.06,0.04-0.07,0.09-0.05,0.14s0.04,0.09,0.05,0.1
c-0.3,0.07-0.47,0.22-0.49,0.44s-0.02,0.39-0.01,0.49c0,0.08-0.07,0.18-0.21,0.3s-0.24,0.23-0.31,0.34c-0.03,0.14,0,0.29,0.08,0.47
c0.08,0.17,0.08,0.27,0,0.3c-0.08-0.01-0.22-0.12-0.41-0.32c-0.19-0.2-0.55-0.24-1.08-0.13c-0.19,0.04-0.39,0.16-0.6,0.36
s-0.23,0.49-0.06,0.88c0.21,0.32,0.43,0.38,0.68,0.19c0.25-0.19,0.43-0.3,0.56-0.31c0.1,0.08,0.12,0.2,0.06,0.35
c-0.06,0.15-0.08,0.24-0.08,0.27c0.06-0.01,0.17,0.02,0.34,0.09s0.28,0.27,0.32,0.58c0.04,0.31,0.19,0.49,0.46,0.55
s0.45,0.08,0.57,0.07s0.28-0.09,0.48-0.24s0.32-0.24,0.37-0.27s0.17-0.06,0.35-0.1s0.47,0.03,0.84,0.23s0.67,0.31,0.88,0.35
s0.38,0.11,0.5,0.2s0.18,0.2,0.18,0.33s0.05,0.23,0.14,0.3c0.15,0.04,0.39,0.07,0.72,0.07s0.57,0.1,0.72,0.28
c0.07,0.28-0.08,0.73-0.46,1.38c-0.37,0.64-0.68,1.05-0.93,1.23c-0.18,0.12-0.34,0.29-0.48,0.49s-0.3,0.38-0.49,0.55
s-0.41,0.34-0.66,0.52s-0.51,0.34-0.78,0.5c-0.21,0.15-0.36,0.32-0.46,0.51s-0.19,0.32-0.27,0.4c1.81-0.43,3.28-1.34,4.43-2.75
C21.31,17.6,21.9,15.93,21.95,14z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

-41
View File
@@ -1,41 +0,0 @@
<!-- and guess where these are defined? Yup, you guessed it: the _config.yml file -->
{% if site.resume_print_social_links %}
<ul>
<!-- GitHub link -->
{% if site.resume_social_links.resume_github_url %}
<li><strong>Github</strong>: {{ site.resume_social_links.resume_github_url }}</li>
{% endif %}
<!-- Twitter link -->
{% if site.resume_social_links.resume_twitter_url %}
<li><strong>Twitter</strong>: {{ site.resume_social_links.resume_twitter_url }}</li>
{% endif %}
<!-- Dribbble link -->
{% if site.resume_social_links.resume_dribbble_url %}
<li><strong>Dribbble</strong>: {{ site.resume_social_links.resume_dribbble_url }}</li>
{% endif %}
<!-- Facebook link -->
{% if site.resume_social_links.resume_facebook_url %}
<li><strong>Facebook</strong>: {{ site.resume_social_links.resume_facebook_url }}</li>
{% endif %}
<!-- LinkedIn link -->
{% if site.resume_social_links.resume_linkedin_url %}
<li><strong>LinkedIn</strong>: {{ site.resume_social_links.resume_linkedin_url }}</li>
{% endif %}
<!-- Instagram link -->
{% if site.resume_social_links.resume_instagram_url %}
<li><strong>Instagram</strong>: {{ site.resume_social_links.resume_instagram_url }}</li>
{% endif %}
<!-- Website link -->
{% if site.resume_social_links.resume_website_url %}
<li><strong>Website</strong>: {{ site.resume_social_links.resume_website_url }}</li>
{% endif %}
</ul>
{% endif %}
-254
View File
@@ -1,254 +0,0 @@
<!DOCTYPE html>
<html>
{% include head.html %}
<body class="theme-{% if site.resume_theme %}{{ site.resume_theme }}{% endif %}">
<div class="wrapper" itemscope itemtype="http://schema.org/Person">
<meta itemprop="telephone" content="{{ site.resume_contact_telephone }}"/>
<meta itemprop="address" content="{{ site.resume_contact_address }}"/>
<header class="page-header">
<!-- You can turn off the avatar in _config.yml by setting to false -->
{% if site.resume_avatar == 'true' %}
<img src="images/avatar.jpg" alt="my photo" class="avatar no-print" itemprop="image">
{% endif %}
<!-- Your name is defined in the _config.yml file -->
<h1 class="header-name" itemprop="name">{{ site.resume_name }}</h1>
<!-- To display contact info here, change `display_header_contact_info` value in _config.yml to true -->
{% if site.display_header_contact_info == 'true' %}
<div class="header-contact-info">
<p>{{ site.resume_header_contact_info }}</p>
</div>
{% endif %}
<div class="title-bar no-print">
<!-- Your title is also defined in the _config.yml file -->
<h2 class="header-title" itemprop="jobTitle">{{ site.resume_title }}</h2>
<!-- This is the markup for the icon links; moved out to an include because it's very verbose, and you shouldn't ever need to edit the markup (unless you want to re-order the icons); if you want to customize which links appear, define them in the _config.yml file -->
{% include icon-links.html %}
</div>
<div class="executive-summary" itemprop="description">
{{ site.resume_header_intro }}
</div>
</header>
{% if site.resume_section_experience %}
<!-- begin Experience -->
<section class="content-section">
<header class="section-header">
<h2>Experience</h2>
</header>
{% for job in site.data.experience %}
<div class="resume-item" itemscope itemprop="worksFor" itemtype="http://schema.org/Organization">
<h3 class="resume-item-title" itemprop="name">{{ job.company }}</h3>
<h4 class="resume-item-details" itemprop="description">{{ job.position }} &bull; {{ job.duration }}</h4>
<p class="resume-item-copy">{{ job.summary }}</p>
</div><!-- end of resume-item -->
{% endfor %}
</section>
<!-- end Experience -->
{% endif %}
{% if site.resume_section_publications %}
<!-- begin Publications -->
<section class="content-section">
<header class="section-header">
<h2>Publications</h2>
</header>
{% for publication in site.data.publications %}
<div class="resume-item" itemscope itemtype="http://schema.org/CreativeWork">
<meta itemprop="creator" content="{{ site.resume_name }}" itemtype="http://schema.org/Person" />
<h3 class="resume-item-copy" itemprop="name">{% if publication.url %}<a href="{{ publication.url }}" itemprop="url">{{ publication.name }}</a>{% else %}{{ publication.name }}{% endif %}</h3>
<p class="resume-item-copy">{{ publication.description }}</p>
</div>
{% endfor %}
</section>
<!-- end Publications -->
{% endif %}
{% if site.resume_section_education %}
<!-- begin Education -->
<section class="content-section">
<header class="section-header">
<h2>Education</h2>
</header>
{% for education in site.data.education %}
<div class="resume-item" itemscope itemprop="alumniOf" itemtype="http://schema.org/CollegeOrUniversity">
<h3 class="resume-item-title" itemprop="name">{{ education.uni }}</h3>
<h4 class="resume-item-details group" itemprop="description">{{ education.degree }} &bull; {{ education.year }}</h4>
<h5 class="resume-item-details award-title" itemprop="description">{{ education.award }}</h5>
<p class="resume-item-copy" itemprop="description">
<ul class="resume-item-list">
{% for award in education.awards %}
<li>{{ award.award }}</li>
{% endfor %}
</ul>
<p class="resume-item-copy">{{ education.summary }}</p>
</div>
{% endfor %}
</section>
<!-- end Education -->
{% endif %}
{% if site.resume_section_projects %}
<!-- begin Projects -->
<section class="content-section">
<header class="section-header">
<h2>Projects</h2>
</header>
{% for project in site.data.projects %}
<div class="resume-item" itemscope itemtype="http://schema.org/CreativeWork">
<meta itemprop="creator" content="{{ site.resume_name }}" itemtype="http://schema.org/Person" />
<h3 class="resume-item-title" itemprop="name">{% if project.url %}<a href="{{ project.url }}" itemprop="url">{{ project.project }}</a>{% else %}{{ project.project }}{% endif %}</h3>
<h4 class="resume-item-details" itemprop="description">{{ project.role }} &bull; {{ project.duration }}</h4>
<p class="resume-item-copy">{{ project.description }}</p>
</div>
{% endfor %}
</section>
<!-- end Projects -->
{% endif %}
{% if site.resume_section_skills %}
<!-- begin Skills -->
<section class="content-section">
<header class="section-header">
<h2>Skills</h2>
</header>
{% for skill in site.data.skills %}
<div class="resume-item">
<h4 class="resume-item-details">{{ skill.skill }}</h4>
<p class="resume-item-copy">{{ skill.description }}</p>
</div>
{% endfor %}
</section>
<!-- end Skills -->
{% endif %}
{% if site.resume_section_recognition %}
<!-- begin Recognition -->
<section class="content-section">
<header class="section-header">
<h2>Recognition</h2>
</header>
{% for recognition in site.data.recognitions %}
<div class="resume-item">
<h3 class="resume-item-title" itemprop="award">{{ recognition.award }}</h3>
<h4 class="resume-item-details">{{ recognition.organization }} &bull; {{ recognition.year }}</h4>
<p class="resume-item-copy">{{ recognition.summary }}</p>
</div>
{% endfor %}
</section>
<!-- end Recognition -->
{% endif %}
{% if site.resume_section_associations %}
<!-- begin Associations -->
<section class="content-section">
<header class="section-header">
<h2>Associations</h2>
</header>
{% for association in site.data.associations %}
<div class="resume-item" itemscope itemprop="memberOf" itemtype="http://schema.org/Organization">
<h3 class="resume-item-title" itemprop="name">{% if association.url %}<a href="{{ association.url }}">{{ association.organization }}</a>{% else %}{{ association.organization }}{% endif %}</h3>
<h4 class="resume-item-details" itemprop="description">{{ association.role }} &bull; {{ association.year }}</h4>
<p class="resume-item-copy">{{ association.summary }}</p>
</div>
{% endfor %}
</section>
<!-- end Associations -->
{% endif %}
{% if site.resume_section_interests %}
<!-- begin Interests -->
<section class="content-section">
<header class="section-header">
<h2>Outside Interests</h2>
</header>
<div class="resume-item">
<ul class="resume-item-list">
{% for interest in site.data.interests %}
<li>{{ interest.description }}</li>
{% endfor %}
</ul>
</div>
</section>
<!-- end Interests -->
{% endif %}
{% if site.resume_section_links %}
<!-- begin Links -->
<section class="content-section">
<header class="section-header">
<h2>Additional Links</h2>
</header>
<div class="resume-item">
<ul class="resume-item-list">
{% for link in site.data.links %}
<li><a href={{ link.url }} itemprop="url">{{ link.description }}</a></li>
{% endfor %}
</ul>
</div>
</section>
<!-- end Links -->
{% endif %}
{% if site.resume_print_social_links %}
<!-- begin Print Social Links -->
<section class="content-section print-only">
<header class="section-header">
<h2>Social Links</h2>
</header>
<div class="resume-item">
{% include print-social-links.html %}
</div>
</section>
<!-- end Print Social Links -->
{% endif %}
<footer class="page-footer">
<p class="footer-line">Theme credit to <a href="http://twitter.com/jglovier">@jglovier</a>. Fork me on <a href="https://github.com/jglovier/resume-template">GitHub</a>.</p>
<p class="footer-line">Pengzhan.dev - 2023</p>
</footer>
</div>
</body>
</html>
-46
View File
@@ -1,46 +0,0 @@
// Base styles
// ---------------------------------------/
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
background: $white;
}
body{
@include serif;
color: $text_color;
font-size: 16px;
line-height: 1.5;
}
.wrapper:before,
.wrapper:after { content:""; display:table; }
.wrapper:after { clear: both; }
.wrapper {
max-width: 728px;
padding: 0 24px;
margin: 0 auto;
zoom: 1;
}
// clearfix (now called group,
// via http://css-tricks.com/snippets/css/clear-fix/)
.group:before,
.group:after { content:""; display:table; }
.group:after { clear:both; }
.group { zoom:1; /* For IE 6/7 (trigger hasLayout) */ }
::-moz-selection {
background: rgba(51,51,51,.8);
color: #fff;
text-shadow: none;
}
::selection {
background: rgba(51,51,51,.8);
color: #fff;
text-shadow: none;
}
-74
View File
@@ -1,74 +0,0 @@
// Layout styles
// ---------------------------------------/
// Layout from Primer (https://github.com/primer/primer/blob/master/scss/_layout.scss)
// Fixed-width, centered column for site content.
.container {
width: $container-width;
margin-right: auto;
margin-left: auto;
@include clearfix;
}
// Grid system
//
// Create rows with `.columns` to clear the floated columns and outdent the
// padding on `.column`s with negative margin for alignment.
.columns {
margin-right: -$grid-gutter;
margin-left: -$grid-gutter;
@include clearfix;
}
// Base class for every column (requires a column width from below)
.column {
float: left;
padding-right: $grid-gutter;
padding-left: $grid-gutter;
}
// Column widths
.one-third {
width: 33.333333%;
}
.two-thirds {
width: 66.666667%;
}
.one-fourth {
width: 25%;
}
.one-half {
width: 50%;
}
.three-fourths {
width: 75%;
}
.one-fifth {
width: 20%;
}
.four-fifths {
width: 80%;
}
// Single column hack
.single-column {
padding-right: $grid-gutter;
padding-left: $grid-gutter;
}
// Equal width columns via table sorcery.
.table-column {
display: table-cell;
width: 1%;
padding-right: $grid-gutter;
padding-left: $grid-gutter;
vertical-align: top;
}
-103
View File
@@ -1,103 +0,0 @@
// Mixins
// ---------------------------------------/
@mixin border-radius($radius) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
border-radius: $radius;
}
@mixin transition($value) {
-webkit-transition: $value;
-moz-transition: $value;
transition: $value;
}
// Clearfix
//
// Clears floats via mixin (avoid using as a class).
@mixin clearfix {
&:before {
display: table;
content: "";
}
&:after {
display: table;
clear: both;
content: "";
}
}
// media querie tools
@mixin media_max($screen_width) {
@media (max-width: $screen_width) { @content; }
}
@mixin media_min($screen_width) {
@media (min-width: $screen_width) { @content; }
}
@mixin media_larger_than_mobile {
@media (min-width: 600px) { @content; }
}
@mixin media_mobile {
@media (max-width: 600px) { @content; }
}
// type utilities
@mixin sans {
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
@mixin sans_light {
@include sans;
font-weight: 300;
}
@mixin sans_regular {
@include sans;
font-weight: 400;
}
@mixin sans_bold {
@include sans;
font-weight: 700;
}
@mixin sans_extrabold {
@include sans;
font-weight: 800;
}
@mixin serif {
font-family: "Lora", "Minion Pro", Palatino, Georgia, serif;
}
@mixin serif_regular {
@include serif;
font-weight: 400;
}
@mixin serif_bold {
@include serif;
font-weight: 700;
}
// layout
@mixin section_border {
border-top: 4px solid #c7c7c7;
border-bottom: 2px solid #c7c7c7;
padding: .2rem 0 .4rem;
}
@mixin section_border_thin {
border-top: 1px solid #c7c7c7;
border-bottom: 1px solid #c7c7c7;
padding: .2rem 0 .2rem;
}
-426
View File
@@ -1,426 +0,0 @@
// Normalize styles
// ---------------------------------------/
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS and IE text size adjust after device orientation change,
* without disabling user zoom.
*/
html {
font-family: sans-serif; /* 1 */
text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability of focused elements when they are also in an
* active/hover state.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
box-sizing: content-box; /* 2 */
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
-234
View File
@@ -1,234 +0,0 @@
// Resume styles
// ---------------------------------------/
// util
.section-header {
@include section_border;
margin-bottom: 1.5rem;
h2 {
@include sans;
font-weight: 900;
font-size: 1.75rem;
letter-spacing: -.05rem;
margin: 0;
}
}
// page header styles
.page-header { padding: 1.5rem 0;
.avatar {
width: 95px;
max-width: 100%;
overflow: hidden;
@include border-radius(100px);
margin: 0 auto;
display: block;
}
.header-name {
@include sans;
font-size: 3rem;
font-weight: 900;
text-align: center;
letter-spacing: -.15rem;
line-height: 1;
margin: 1rem 0 0;
}
.header-contact-info {
@include sans;
text-align: center;
}
.header-contact-info p a {
color: inherit;
}
.title-bar {
@include section_border;
margin-top: 2rem;
padding: .4rem 0;
@include clearfix;
.header-title {
@include sans;
font-weight: 300;
font-size: 1.75rem;
line-height: 2rem;
letter-spacing: -.05rem;
text-align: center;
margin: 0 0 .5rem;
@include media_larger_than_mobile {
float: left;
margin: 0;
}
}
}
.executive-summary {
@include media_larger_than_mobile { font-size: 1.125rem; }
}
}
.icon-links {
margin: 0;
padding: 0;
list-style: none;
position: relative;
top: 4px;
text-align: center;
border-top: 2px solid #c7c7c7;
padding-top: .4rem;
@include media_larger_than_mobile {
float: right;
border-top: none;
padding-top: 0;
}
.icon-link-item {
display: inline-block;
margin-left: 5px;
}
.icon-link { display: inline-block;
&:hover .icon path {
fill: #333;
@include transition(all .2s ease);
}
}
.icon {
height: 28px;
}
}
.contact-button {
@include border-radius(3px);
background-color: #efefef;
text-decoration: none;
text-align: center;
display: block;
margin: 1.5rem auto;
width: 220px;
font-size: 1.375rem;
@include sans_light;
color: #333;
line-height: 55px;
@include transition(all .2s ease);
&:hover {
background-color: #333;
color: #fff;
}
&.not-looking { width: 400px; }
}
// resume content styles
.content-section {
margin: 0 0 3rem;
.resume-item { margin-bottom: 2rem; }
.resume-item-title {
@include sans_bold;
margin: 0 0 .75rem;
font-size: 1.5rem;
letter-spacing: -.05rem;
line-height: 1;
}
.resume-item-details {
font-size: 1.125rem;
margin: 0 0 .75rem;
line-height: 1;
&.award-title {
font-size: 1rem;
}
}
.resume-item-title,
.resume-item-details,
.resume-item-copy,
.resume-item-list {
a {
color: #333;
text-decoration: none;
border-bottom: 1px solid;
&:hover {
border-bottom: none;
color: #9c9c9c;
}
}
}
.resume-item-copy { margin: .75rem 0 0; }
}
// footer styles
.page-footer {
border-top: 4px solid #c7c7c7;
padding-top: 1.5rem;
text-align: center;
margin-bottom: 3rem;
.footer-line {
margin: 0;
font-size: .875rem;
color: #999;
> a {
font-weight: 700;
color: #333;
}
}
}
.print-only { display: none !important }
@media print {
.no-print { display: none !important }
.print-only { display: block !important }
body { font-size: 12px; line-height: 1rem; }
.page-header { padding: 1rem 0 .2rem;
.header-name {
@include sans_bold;
font-size: 2rem;
margin: 0 0 .2rem;
}
}
.section-header {
@include section_border_thin;
margin-bottom: 1rem;
h2 { font-size: 1.2rem; line-height: 1.5rem; }
}
.content-section {
margin: 0 0 1rem;
.resume-item { margin-bottom: 1rem; }
.resume-item-title {
font-size: 16px;
line-height: .7em;
}
.resume-item-details {
font-size: 12px;
margin: 0 0 .75rem;
line-height: .7em;
font-style:italic;
&.award-title {
font-size: 11px;
}
}
}
}
-21
View File
@@ -1,21 +0,0 @@
// Variables
// ---------------------------------------/
$container-width: 980px !default;
$grid-gutter: 10px !default;
// Colors
$white: #fff;
$black: #333;
// Styles
$text_color: $black;
// Font stack
$body-font: Helvetica, arial, nimbussansl, liberationsans, freesans, clean, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol" !default;
// The base body size
$body-font-size: 13px !default;
// Monospace font stack
$mono-font: Consolas, "Liberation Mono", Menlo, Courier, monospace !default;
-14
View File
@@ -1,14 +0,0 @@
---
---
// "Reset" styles and SCSS reusable components
@import "normalize";
@import "mixins";
@import "variables";
// Generic site base & layout styles
@import "base";
@import "layout";
// Layouts
@import "resume";
-2
View File
@@ -1,2 +0,0 @@
rm -rf /var/www/html
cp -rf _site /var/www/html
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

-3
View File
@@ -1,3 +0,0 @@
---
layout: resume
---
+1273
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "pengzhan-dev",
"version": "1.0.0",
"description": "Dynamic web framework and personal site skeleton for pengzhan.dev",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
"dev": "node --watch --watch-path=server --watch-path=views --watch-path=public server/index.js",
"lint": "node scripts/lint.js",
"test": "node test/test.js"
},
"keywords": ["blog", "k8s", "ssr", "express", "markdown"],
"author": "Pengzhan Hao",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.3",
"ejs": "^3.1.10",
"express": "^4.21.2",
"feed": "^4.2.2",
"gray-matter": "^4.0.3",
"js-yaml": "^4.1.0",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0",
"markdown-it-attrs": "^4.3.1",
"markdown-it-footnote": "^4.0.0",
"prismjs": "^1.29.0"
}
}
+394
View File
@@ -0,0 +1,394 @@
:root {
--color-bg: #f9f9f9;
--color-surface: #ffffff;
--color-surface-soft: #f4f4f5;
--color-text-main: #111111;
--color-text-secondary: #4b5563;
--color-text-muted: #6b7280;
--color-border: #e5e7eb;
--color-border-hover: #d1d5db;
--color-accent: #111111;
--color-mark-bg: #fef08a;
--color-mark-text: #111111;
--font-serif: 'Cardo', 'Iowan Old Style', 'Palatino Linotype', 'Songti SC', 'STSong', Georgia, serif;
--font-mono: 'Maple Mono NF CN', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
--max-width: 1180px;
--content-width: 820px;
--header-height: 80px;
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 20px;
--radius-full: 9999px;
--transition-fast: 0.15s ease;
--transition-normal: 0.25s ease;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
font-family: var(--font-serif);
color: var(--color-text-main);
background-color: var(--color-bg);
line-height: 1.8;
letter-spacing: 0.01em;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
width: 100%;
max-width: var(--max-width);
margin: 0 auto;
padding: 0 2rem;
}
.content-container {
width: 100%;
max-width: var(--content-width);
margin: 0 auto;
}
/* Header */
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: rgba(249, 249, 249, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
height: var(--header-height);
display: flex;
align-items: center;
}
.header-inner {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.brand {
text-decoration: none;
color: var(--color-text-main);
}
.brand-title {
font-size: 1.55rem;
font-family: var(--font-serif);
font-weight: 700;
letter-spacing: -0.01em;
}
.site-nav {
display: flex;
gap: 2rem;
align-items: center;
}
.nav-link {
text-decoration: none;
font-family: var(--font-serif);
font-size: 1.05rem;
color: var(--color-text-main);
transition: opacity var(--transition-fast);
padding: 0.25rem 0;
}
.nav-link:hover {
opacity: 0.65;
}
.nav-link.active {
text-decoration: underline;
text-underline-offset: 4px;
}
/* Mobile Hamburger Menu Toggle */
.mobile-menu-toggle {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 0.5rem;
flex-direction: column;
justify-content: center;
gap: 5px;
width: 40px;
height: 40px;
}
.hamburger-line {
display: block;
width: 22px;
height: 2px;
background-color: var(--color-text-main);
border-radius: 2px;
transition: transform 0.25s ease, opacity 0.25s ease;
}
.mobile-menu-toggle.is-active .hamburger-line:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.mobile-menu-toggle.is-active .hamburger-line:nth-child(2) {
opacity: 0;
}
.mobile-menu-toggle.is-active .hamburger-line:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
/* Mobile Slide-Down Navigation Drawer */
.mobile-nav-drawer {
display: none;
position: absolute;
top: 100%;
left: 0;
width: 100%;
background: var(--color-bg);
border-bottom: 1px solid var(--color-border);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
padding: 0.75rem 1.25rem 1.25rem;
z-index: 999;
}
.mobile-nav-drawer.is-open {
display: block;
animation: slideDown 0.25s ease;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.mobile-nav-links {
display: flex;
flex-direction: column;
}
.mobile-nav-link {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.85rem 0.5rem;
text-decoration: none;
color: var(--color-text-main);
font-family: var(--font-serif);
font-size: 1.15rem;
border-bottom: 1px solid var(--color-border-subtle, rgba(0, 0, 0, 0.05));
transition: color var(--transition-fast), padding-left var(--transition-fast);
}
.mobile-nav-link:last-child {
border-bottom: none;
}
.mobile-nav-link:hover, .mobile-nav-link:active {
color: var(--color-accent);
padding-left: 0.75rem;
}
.mobile-nav-link.active {
font-weight: 600;
color: var(--color-accent);
}
.mobile-nav-arrow {
font-size: 1rem;
opacity: 0.4;
}
/* Main Content area */
.site-main {
flex: 1;
padding: 2.5rem 0 4rem;
}
/* Footer */
.site-footer {
border-top: 1px solid var(--color-border);
background-color: var(--color-surface);
padding: 2.5rem 0;
font-family: var(--font-sans);
font-size: 0.88rem;
color: var(--color-text-muted);
}
.footer-inner {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1.5rem;
}
.social-links {
display: flex;
gap: 1.25rem;
align-items: center;
}
.social-link {
color: var(--color-text-secondary);
display: flex;
align-items: center;
justify-content: center;
transition: color var(--transition-fast), transform var(--transition-fast);
}
.social-link:hover {
color: var(--color-text-main);
transform: translateY(-2px);
}
.social-link svg {
width: 19px;
height: 19px;
fill: currentColor;
}
/* Headings & Typography */
h1, h2, h3, h4, h5, h6 {
color: var(--color-text-main);
line-height: 1.35;
margin-bottom: 1rem;
font-weight: 700;
}
h1 { font-size: 2.2rem; }
h2 { font-size: 1.65rem; margin-top: 2rem; }
h3 { font-size: 1.35rem; margin-top: 1.5rem; }
p {
margin-bottom: 1.25rem;
}
a {
color: var(--color-accent);
text-decoration: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
transition: color var(--transition-fast);
}
a:hover {
color: #000000;
}
mark {
background-color: var(--color-mark-bg);
color: var(--color-mark-text);
padding: 0.1em 0.25em;
border-radius: 3px;
font-weight: 500;
}
blockquote {
border-left: 3px solid var(--color-border-hover);
padding: 0.5rem 0 0.5rem 1.25rem;
margin: 1.5rem 0;
color: var(--color-text-secondary);
font-style: italic;
}
hr {
border: 0;
height: 1px;
background: var(--color-border);
margin: 2.5rem 0;
}
/* Responsive */
@media (max-width: 768px) {
:root {
--header-height: 64px;
--content-padding: 1.25rem;
}
.site-header {
position: sticky;
top: 0;
z-index: 990;
}
/* Hide inline desktop nav and show hamburger */
.site-nav.desktop-nav {
display: none;
}
.mobile-menu-toggle {
display: flex;
}
.brand-title {
font-size: 1.25rem;
}
.brand-tagline {
font-size: 0.68rem;
display: block;
margin-top: 0.1rem;
opacity: 0.85;
}
/* Centered, balanced editorial mobile footer */
.site-footer {
padding: 2.5rem 1.25rem 3rem;
}
.footer-inner {
flex-direction: column;
align-items: center;
text-align: center;
gap: 1.4rem;
}
.footer-left {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.45rem;
}
.footer-tagline {
font-family: var(--font-serif);
font-size: 1.05rem;
color: var(--color-text-main);
}
.footer-copyright {
font-size: 0.82rem;
color: var(--color-text-muted);
}
.social-links {
justify-content: center;
gap: 1.5rem;
}
.social-link {
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
}
}
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
/* Fonts definitions with graceful local fallbacks */
@font-face {
font-family: 'XiaoheSimplifySerif-VF';
src: url('/fonts/XiaoheSimplifySerif-VF.otf') format('opentype');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Maple Mono NF CN';
src: url('/fonts/MapleMono-NF-CN-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Maple Mono NF CN';
src: url('/fonts/MapleMono-NF-CN-Bold.ttf') format('truetype');
font-weight: 700;
font-style: normal;
font-display: swap;
}
+118
View File
@@ -0,0 +1,118 @@
/**
* Okaidia PrismJS theme matching WordPress Prismatic plugin
*/
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
font-family: 'Maple Mono NF CN', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.95em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.6;
tab-size: 2;
hyphens: none;
}
pre[class*="language-"] {
padding: 1.25rem 1.5rem;
margin: 1.75rem 0;
overflow: auto;
border-radius: 12px;
background: #272822;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #272822;
}
/* Inline code outside pre */
:not(pre) > code {
padding: 0.2em 0.4em;
border-radius: 4px;
color: #e83e8c;
background-color: rgba(0, 0, 0, 0.05);
font-family: 'Maple Mono NF CN', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.88em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #8292a2;
}
.token.punctuation {
color: #f8f8f2;
}
.token.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #f92672;
}
.token.boolean,
.token.number {
color: #ae81ff;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #a6e22e;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #f8f8f2;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.class-name {
color: #e6db74;
}
.token.keyword {
color: #66d9ef;
}
.token.regex,
.token.important {
color: #fd971f;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
+49
View File
@@ -0,0 +1,49 @@
/**
* Mobile Navigation Drawer Controller
*/
document.addEventListener('DOMContentLoaded', () => {
const toggleBtn = document.getElementById('mobile-menu-toggle');
const drawer = document.getElementById('mobile-nav-drawer');
if (toggleBtn && drawer) {
toggleBtn.addEventListener('click', () => {
const isOpen = drawer.classList.contains('is-open');
if (isOpen) {
drawer.classList.remove('is-open');
toggleBtn.classList.remove('is-active');
toggleBtn.setAttribute('aria-expanded', 'false');
} else {
drawer.classList.add('is-open');
toggleBtn.classList.add('is-active');
toggleBtn.setAttribute('aria-expanded', 'true');
}
});
// Close on navigation link click
drawer.querySelectorAll('.mobile-nav-link').forEach(link => {
link.addEventListener('click', () => {
drawer.classList.remove('is-open');
toggleBtn.classList.remove('is-active');
toggleBtn.setAttribute('aria-expanded', 'false');
});
});
// Close when clicking outside
document.addEventListener('click', (e) => {
if (drawer.classList.contains('is-open') && !drawer.contains(e.target) && !toggleBtn.contains(e.target)) {
drawer.classList.remove('is-open');
toggleBtn.classList.remove('is-active');
toggleBtn.setAttribute('aria-expanded', 'false');
}
});
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && drawer.classList.contains('is-open')) {
drawer.classList.remove('is-open');
toggleBtn.classList.remove('is-active');
toggleBtn.setAttribute('aria-expanded', 'false');
}
});
}
});
+20
View File
@@ -0,0 +1,20 @@
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('search-input');
const searchForm = document.getElementById('search-form');
const pillElements = document.querySelectorAll('.search-pill-btn');
if (searchInput && pillElements.length > 0) {
pillElements.forEach(pill => {
pill.addEventListener('click', (e) => {
e.preventDefault();
const query = pill.getAttribute('data-query');
if (query) {
searchInput.value = query;
if (searchForm) {
searchForm.submit();
}
}
});
});
}
});
+129
View File
@@ -0,0 +1,129 @@
/**
* Interactive Table of Contents Controller
* - Perfectly Centered Post Layout (No 70/30 split)
* - Margin-Docked Auto-Hide / Minimizable Desktop Rail (Dropbox Paper style)
* - Mobile / Tablet Floating Action Badge & Slide-Up Bottom Sheet
*/
document.addEventListener('DOMContentLoaded', () => {
const sideToc = document.getElementById('side-toc');
const minimizeBtn = document.getElementById('side-toc-minimize');
const floatingBadge = document.getElementById('floating-toc-badge');
const sheet = document.getElementById('mobile-toc-sheet');
const overlay = document.getElementById('mobile-toc-overlay');
const sheetCloseBtn = document.getElementById('mobile-sheet-close');
// Check user preference for minimized TOC in localStorage
let isMinimized = false;
try {
isMinimized = localStorage.getItem('pengzhan_toc_minimized') === 'true';
} catch (_) {}
function applyDesktopState() {
if (!sideToc) return;
if (window.innerWidth >= 1280) {
if (isMinimized) {
sideToc.classList.add('is-minimized');
if (floatingBadge) floatingBadge.classList.add('is-visible');
} else {
sideToc.classList.remove('is-minimized');
if (floatingBadge) floatingBadge.classList.remove('is-visible');
}
} else {
// On screens < 1280px, desktop side rail is hidden via CSS, floating badge is available
if (floatingBadge) floatingBadge.classList.add('is-visible');
}
}
if (minimizeBtn) {
minimizeBtn.addEventListener('click', () => {
isMinimized = true;
try { localStorage.setItem('pengzhan_toc_minimized', 'true'); } catch (_) {}
applyDesktopState();
});
}
// Floating badge click
if (floatingBadge) {
floatingBadge.addEventListener('click', () => {
if (window.innerWidth >= 1280) {
// Expand desktop side TOC
isMinimized = false;
try { localStorage.setItem('pengzhan_toc_minimized', 'false'); } catch (_) {}
applyDesktopState();
} else {
// Open mobile bottom sheet
openMobileSheet();
}
});
}
function openMobileSheet() {
if (sheet && overlay) {
sheet.classList.add('is-open');
overlay.classList.add('is-open');
document.body.style.overflow = 'hidden';
}
}
function closeMobileSheet() {
if (sheet && overlay) {
sheet.classList.remove('is-open');
overlay.classList.remove('is-open');
document.body.style.overflow = '';
}
}
if (sheetCloseBtn) sheetCloseBtn.addEventListener('click', closeMobileSheet);
if (overlay) overlay.addEventListener('click', closeMobileSheet);
// Close mobile sheet on anchor click
if (sheet) {
sheet.querySelectorAll('.mobile-sheet-link').forEach(link => {
link.addEventListener('click', () => {
closeMobileSheet();
});
});
}
window.addEventListener('resize', applyDesktopState, { passive: true });
applyDesktopState();
// Scroll Spy for Desktop Rail and Mobile List
const sideLinks = Array.from(document.querySelectorAll('.side-toc-link'));
if (sideLinks.length === 0) return;
const headings = sideLinks
.map(link => {
const id = link.getAttribute('data-id');
const target = document.getElementById(id);
return target ? { id, target, link } : null;
})
.filter(Boolean);
if (headings.length === 0) return;
function updateActiveHeading() {
const scrollPos = window.scrollY + 140;
let currentActive = headings[0];
for (let i = 0; i < headings.length; i++) {
const top = headings[i].target.getBoundingClientRect().top + window.scrollY;
if (top <= scrollPos) {
currentActive = headings[i];
} else {
break;
}
}
headings.forEach(item => {
if (item === currentActive) {
item.link.classList.add('toc-active');
} else {
item.link.classList.remove('toc-active');
}
});
}
window.addEventListener('scroll', updateActiveHeading, { passive: true });
updateActiveHeading();
});
+43
View File
@@ -0,0 +1,43 @@
site:
title: "Pengzhan"
tagline: "Was childish, Now foolish."
url: "https://pengzhan.dev"
author:
name: "Pengzhan Hao"
subtitle: "Software engineer on Cloud"
gravatar: "1f0283e19ce9680f2441b9c37211e0b890fd7ebf4b9ea5a5c413fe0379d7d3f9"
admin:
secret: "admin" # Admin dashboard password (or set via ADMIN_SECRET env var)
navigation:
- label: "Home"
path: "/"
- label: "Blogs"
path: "/blogs"
- label: "Search"
path: "/search"
- label: "Quick Links"
path: "/quick-links"
social:
- platform: github
url: "https://github.com/CoderSherlock"
- platform: instagram
url: "https://www.instagram.com/pengzhan.hao/"
- platform: linkedin
url: "https://www.linkedin.com/in/pengzhanhao/"
- platform: email
address: "[email protected]"
search:
recommended:
- label: 'tag:k8s "container image" category:tech'
query: 'tag:k8s "container image" category:tech'
plugins:
google-analytics:
enabled: true
tracking_id: "G-MNP4QN1T9T"
reading-time:
enabled: true
+2
View File
@@ -0,0 +1,2 @@
/* Custom CSS overrides on Persistent Volume */
/* Any CSS written here will be loaded after base and components CSS */
+104
View File
@@ -0,0 +1,104 @@
---
title: "Pengzhan Hao"
layout: home
permalink: "/"
---
<div class="home-hero-section">
<h1 class="hero-name">Pengzhan Hao</h1>
<p class="hero-subtitle">Software engineer on Cloud</p>
<div class="hero-intro">
<p>Joined Google in 2022, working in GKE, and focusing on Node &amp; runtime area. Most projects landed on bringing ARM-based VMs to GKE, introduced more flexibilities in node customization and pod lifecycle improvement for preemptible VMs</p>
<p>Contributed to Kubernetes open source community and became a member in 2023</p>
<p>Now working on making GKE a more productive platform for AI/ML</p>
</div>
</div>
<section class="home-section">
<h2 class="section-title">Research and study life in Binghamton University</h2>
<div class="two-column-grid">
<div class="column-block">
<p>Wide interests in multiple research areas, and spend most of my time in Edge computing, distributed systems and networks, and mobile operating system research. Under supervision from Professor Yifan Zhang, I spent 7 years in these research areas and outcome publications</p>
</div>
<div class="column-block">
<p>I worked as a teaching assistant for Operating system and Distributed system courses for both graduate and undergraduate levels. Mostly, I helped lecturers finish academic goals and hosted Labs for students. Before and during my research career, I finished my Master’s level education with a full grade point average.</p>
</div>
</div>
</section>
<section class="home-section">
<h2 class="section-title">Publications</h2>
<div class="publications-list">
<div class="pub-item">
<a href="/documents/eddl-sec21.pdf" target="_blank" class="pub-title">EDDL: A Distributed Deep Learning System for Resource-limited Edge Computing Environment</a>
<p class="pub-meta">Pengzhan Hao, and Yifan Zhang &bull; The Sixth ACM/IEEE Symposium on Edge Computing (ACM/IEEE SEC) &bull; San Jose, CA, USA, 2021.</p>
</div>
<div class="pub-item">
<a href="/documents/eta-infocom18.pdf" target="_blank" class="pub-title">A Case for Web Service Bandwidth Reduction on Mobile Devices with Edge-hosted Personal Services</a>
<p class="pub-meta">Yongshu Bai, Pengzhan Hao, and Yifan Zhang &bull; The 37th IEEE International Conference on Computer Communications (IEEE INFOCOM) &bull; Honolulu, HI, USA, 2018.</p>
</div>
<div class="pub-item">
<a href="/documents/a7-hao.pdf" target="_blank" class="pub-title">EdgeCourier: An Edge-hosted Personal Service for Low-bandwidth Document Synchronization in Mobile Cloud Storage Services</a>
<p class="pub-meta">Pengzhan Hao, Yongshu Bai, Xin Zhang, and Yifan Zhang &bull; The 2nd ACM/IEEE Symposium on Edge Computing (ACM/IEEE SEC) &bull; San Jose, CA, USA, 2017.</p>
</div>
<div class="pub-item">
<a href="/documents/p169-zhang.pdf" target="_blank" class="pub-title">Poster: Securing Device Inputs for Smartphones Using Hypervisor Based Approach</a>
<p class="pub-meta">Xin Zhang, Yongshu Bai, Pengzhan Hao, and Yifan Zhang &bull; The 15th ACM International Conference on Mobile Systems, Applications, and Services (ACM MobiSys) &bull; Niagara Falls, NY, USA, 2017.</p>
</div>
<div class="pub-item">
<a href="/documents/p163-hao.pdf" target="_blank" class="pub-title">Poster: EPS – Edge-hosted Personal Services for Mobile Users</a>
<p class="pub-meta">Pengzhan Hao, Yongshu Bai, Xin Zhang, and Yifan Zhang &bull; The 15th ACM International Conference on Mobile Systems, Applications, and Services (ACM MobiSys) &bull; Niagara Falls, NY, USA, 2017.</p>
</div>
</div>
</section>
<section class="home-section">
<h2 class="section-title">Undergraduate life in Beihang University</h2>
<div class="two-column-grid">
<div class="column-block">
<p>From 2011 to 2015, I attended the Software Engineering program at Beihang University. Overall, I received 3.25 out of 4 when graduating. During most of the era, I was enthusiasm in participating Movie Association and Mountain Climbing Association, and also worked as student assistant for career program for the last year.</p>
</div>
<div class="column-block">
<p>Starting from my junior year, I studied and researched consecutively in two labs. Worked in Android Kernel and module localization and smart urban computing areas. In urban computing areas, led a small team to participate in a joint project together by the Advanced Computer application technology engineering research center(Beihang University) and Microsoft Research Asia(MSRA).</p>
</div>
</div>
</section>
<section class="home-section">
<h2 class="section-title">Projects</h2>
<div class="projects-list">
<div class="project-item">
<h3 class="project-name">EDDL &ndash; Edge distributed deep learning framework</h3>
<p>A lite-weight c++ written framework(based on Dlib) for distributed neural network training on embedded devices and mobile phones. With supporting of our implemented runtime profilers, training devices can interconnect in an optimized network topology under various network environments. In our last publication, we showed our framework can perform distributed training for diverse neural networks on resource-limited devices. The EDDL framework also showed well scalability in large amounts of devices. We are currently working on network optimization on per-batch communication time and overall convergence time.</p>
</div>
<div class="project-item">
<h3 class="project-name">Edge Courier</h3>
<p>A proxy-based protocol for incremental file synchronization and cooperative online document editing. This protocol supports file hosting services including Google Drive, Dropbox, and OneDrive as well as document collaboration services such as G Suite and Microsoft 365. Edge courier can significantly decrease bandwidth consumption for collaborative editing web apps, and also showed advancement in cloud file storage services.</p>
</div>
<div class="project-item">
<h3 class="project-name">RTRouting assistant</h3>
<p>A real time routing prototype which can guide vehicle to alternative routes with high-volume traffic prediction. The predicted traffic alert is generated by a pre-trained model learned from history monitored traffics, real time event warnings and a abnormal traffic prediction model. This project belongs to a urban computing project studied together by Advance computer application technology engineering research center(Beihang University) and Microsoft Research Asia(MSRA).</p>
</div>
</div>
</section>
<section class="home-section">
<h2 class="section-title">Recognition</h2>
<div class="recognition-list">
<div class="recog-item">
<strong>ACM/IEEE SEC student travel grant</strong>
<span class="recog-meta">IEEE &bull; 2017, 2021</span>
</div>
<div class="recog-item">
<strong>ACM MobiSys 2017 student travel grant</strong>
<span class="recog-meta">ACM &bull; 2017</span>
</div>
</div>
</section>
+20
View File
@@ -0,0 +1,20 @@
---
title: "Quick Links"
permalink: "/quick-links"
---
A curated collection of public resources, contact points, and references:
## Professional Profiles
- **GitHub**: [github.com/CoderSherlock](https://github.com/CoderSherlock) &mdash; open-source projects, personal tools, and experiments.
- **LinkedIn**: [linkedin.com/in/pengzhanhao](https://www.linkedin.com/in/pengzhanhao/) &mdash; professional background.
- **Instagram**: [@pengzhan.hao](https://www.instagram.com/pengzhan.hao/) &mdash; photography and outdoor life.
## Direct Contact
- **Email**: [[email protected]](mailto:[email protected])
## Documents
- **Curriculum Vitae**: [Download CV (PDF)](/documents/CV.pdf)
@@ -0,0 +1,17 @@
module.exports = {
injectHead({ pluginConfig }) {
const trackingId = pluginConfig.tracking_id;
if (!trackingId) return '';
return `
<!-- Google Analytics (GA4 via Plugin) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=${trackingId}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${trackingId}');
</script>
`;
}
};
@@ -0,0 +1,5 @@
{
"name": "google-analytics",
"version": "1.0.0",
"description": "Injects Google Analytics 4 tracking script into head"
}
@@ -0,0 +1,18 @@
module.exports = {
onPostLoad({ post }) {
const wordCount = (post.rawContent || '').trim().split(/\s+/).length;
// Assume average 200 words per minute
const minutes = Math.max(1, Math.round(wordCount / 200));
post.readingTime = `${minutes} min read`;
post.wordCount = wordCount;
},
injectPostEnd({ post }) {
if (post && post.readingTime) {
return `<div style="font-family: var(--font-sans); font-size: 0.85rem; color: var(--color-text-muted); margin-top: 2rem; padding: 0.75rem 1rem; background: var(--color-surface-soft); border-radius: var(--radius-sm);">
Article statistics: ~${post.wordCount} words &bull; ${post.readingTime}
</div>`;
}
return '';
}
};
@@ -0,0 +1,5 @@
{
"name": "reading-time",
"version": "1.0.0",
"description": "Calculates reading time and word count for articles"
}
@@ -0,0 +1,15 @@
module.exports = {
onPostLoad({ post }) {
const raw = post.rawContent || '';
const internalMatches = raw.match(/\[[^\]]+\]\((\/[^)]+)\)/g) || [];
const externalMatches = raw.match(/\[[^\]]+\]\((https?:\/\/[^)]+)\)/g) || [];
post.seoMetrics = {
internalLinkCount: internalMatches.length,
externalLinkCount: externalMatches.length,
hasMetaDescription: Boolean(post.excerpt && post.excerpt.length > 30),
titleLength: post.title.length,
isTitleOptimal: post.title.length >= 25 && post.title.length <= 70
};
}
};
@@ -0,0 +1,5 @@
{
"name": "seo-analyzer",
"version": "1.0.0",
"description": "Dynamic SEO auditor and content health inspector"
}
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" width="100%" height="100%">
<defs>
<linearGradient id="kidBg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fed7aa"/>
<stop offset="100%" stop-color="#fb923c"/>
</linearGradient>
<filter id="kidShadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="6" stdDeviation="10" flood-opacity="0.15"/>
</filter>
</defs>
<rect width="600" height="600" fill="url(#kidBg)" rx="32"/>
<!-- Creative Building Blocks / Robot Icon -->
<g transform="translate(300, 250)" filter="url(#kidShadow)">
<rect x="-90" y="-80" width="180" height="150" rx="24" fill="#ffffff"/>
<!-- Robot antenna -->
<line x1="0" y1="-80" x2="0" y2="-120" stroke="#ffffff" stroke-width="8" stroke-linecap="round"/>
<circle cx="0" cy="-125" r="14" fill="#ea580c"/>
<!-- Eyes -->
<circle cx="-40" cy="-15" r="16" fill="#0284c7"/>
<circle cx="40" cy="-15" r="16" fill="#0284c7"/>
<!-- Smile -->
<path d="M -40 30 Q 0 60 40 30" stroke="#ea580c" stroke-width="8" stroke-linecap="round" fill="none"/>
</g>
<!-- Educational Blocks -->
<g transform="translate(180, 420)" filter="url(#kidShadow)">
<rect x="0" y="0" width="60" height="60" rx="12" fill="#38bdf8"/>
<text x="30" y="42" text-anchor="middle" fill="#ffffff" font-family="-apple-system, sans-serif" font-size="32" font-weight="900">A</text>
</g>
<g transform="translate(260, 400)" filter="url(#kidShadow)">
<rect x="0" y="0" width="60" height="60" rx="12" fill="#ec4899"/>
<text x="30" y="42" text-anchor="middle" fill="#ffffff" font-family="-apple-system, sans-serif" font-size="32" font-weight="900">I</text>
</g>
<g transform="translate(340, 420)" filter="url(#kidShadow)">
<rect x="0" y="0" width="60" height="60" rx="12" fill="#10b981"/>
<text x="30" y="42" text-anchor="middle" fill="#ffffff" font-family="-apple-system, sans-serif" font-size="32" font-weight="900">&lt;&gt;</text>
</g>
<text x="300" y="530" text-anchor="middle" fill="#7c2d12" font-family="-apple-system, sans-serif" font-size="28" font-weight="800">Coding for Kids</text>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,26 @@
---
title: "AI可以改变什么:为孩子编程与创造力"
date: 2025-05-10
author: "haopengzhan"
category: "Non-tech"
tags: ["AI", "Parenting", "Education", "Kids"]
excerpt: "当AI能够直接生成代码,我们应该如何引导下一代理解技术、培养解决真实问题的创造力?"
cover: images/cover.svg
lang: zh
---
随着人工智能技术的迅猛普及,编程教育的门槛正在发生前所未有的变化。
过去,学习编程首先要克服繁琐的语法、编译器配置和环境搭建;而如今,孩子们只需要用自己的语言描述心中的想法,AI 就能协助将其变为可视化的交互程序。
## 从“写语法”到“定义问题”
在传统的教学中,很多孩子在记忆循环和变量类型时失去了兴趣。生成式 AI 带来的改变在于:
- **即时反馈**:孩子可以随时看到想法被渲染出来。
- **系统性思考**:重点转移为理解逻辑流程与规则边界。
- **激发创造力**:从消费者转变为创造者。
## 结语
技术工具永远在演进,不变的是面对未知问题时的探究欲望与审美能力。
@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" width="100%" height="100%">
<defs>
<linearGradient id="kubeBg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#93c5fd"/>
<stop offset="100%" stop-color="#3b82f6"/>
</linearGradient>
<filter id="kubeShadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="8" stdDeviation="12" flood-opacity="0.25"/>
</filter>
</defs>
<rect width="600" height="600" fill="url(#kubeBg)" rx="32"/>
<!-- Kubernetes / Kubelet Hexagon -->
<g transform="translate(300, 260)" filter="url(#kubeShadow)">
<polygon points="0,-130 112,-65 112,65 0,130 -112,65 -112,-65" fill="#1e40af" stroke="#ffffff" stroke-width="8"/>
<!-- Central Ship Wheel / Helm Spokes -->
<circle cx="0" cy="0" r="45" fill="#3b82f6" stroke="#ffffff" stroke-width="6"/>
<circle cx="0" cy="0" r="18" fill="#ffffff"/>
<!-- Spokes -->
<line x1="0" y1="-75" x2="0" y2="75" stroke="#ffffff" stroke-width="6"/>
<line x1="-65" y1="-37" x2="65" y2="37" stroke="#ffffff" stroke-width="6"/>
<line x1="-65" y1="37" x2="65" y2="-37" stroke="#ffffff" stroke-width="6"/>
</g>
<!-- Large Kubelet Text -->
<text x="300" y="470" text-anchor="middle" fill="#ffffff" font-family="-apple-system, sans-serif" font-size="52" font-weight="900" letter-spacing="-1">Kubelet</text>
<text x="300" y="510" text-anchor="middle" fill="#dbeafe" font-family="-apple-system, sans-serif" font-size="20" font-weight="600" letter-spacing="2">RUNTIME &amp; PLEG DEEP DIVE</text>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,60 @@
---
title: "Kubelet Source Code Deep Dive: SyncLoop and PLEG"
date: 2025-07-15
author: "haopengzhan"
category: "Tech"
tags: ["k8s", "kubernetes", "go", "cloud", "containers"]
excerpt: "An in-depth exploration into the Kubernetes Kubelet runtime architecture, focusing on syncLoop mechanics and Pod Lifecycle Event Generator (PLEG)."
cover: images/cover.svg
lang: en
---
Kubelet is the primary "node agent" that runs on each Kubernetes worker node. It registers the node with the apiserver, watches for PodSpec changes, and ensures containers described in those PodSpecs are running and healthy[^1].
## Architectural Overview
At the heart of the Kubelet daemon is the `syncLoop`. The syncLoop monitors multiple channels for pod changes:
1. **Config channel**: Pods from API server, file manifests, or HTTP endpoints.
2. **PLEG channel**: Pod Lifecycle Event Generator events.
3. **Probe channel**: Liveness and readiness probe results.
4. **Housekeeping channel**: Periodic cleanup tasks.
### The SyncLoop Implementation
Here is a simplified view of how Kubelet receives events in Go:
```go
func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) {
klog.InfoS("Starting kubelet main sync loop")
for {
select {
case u, ok := <-updates:
if !ok {
klog.ErrorS(nil, "Updates channel closed")
return
}
handler.HandlePodAdditions(u.Pods)
case e := <-kl.pleg.Watch():
handler.HandlePLEGEvent(e)
}
}
}
```
## Pod Lifecycle Event Generator (PLEG)
PLEG periodically relists containers from the container runtime via CRI:
```bash
# Inspecting runtime events with crictl
crictl ps --state Running
```
If PLEG does not complete within the expected relist period (default 3 minutes), the node becomes `NotReady` with the condition `PLEG is not healthy`.
## Conclusion
Understanding the syncLoop provides clear insights into why nodes report PLEG issues during disk I/O bottlenecks or heavy container churn.
[^1]: Kubernetes documentation: https://kubernetes.io/docs/concepts/architecture/
@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" width="100%" height="100%">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#cbd5e1"/>
<stop offset="100%" stop-color="#94a3b8"/>
</linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="8" stdDeviation="12" flood-opacity="0.18"/>
</filter>
</defs>
<rect width="600" height="600" fill="url(#bg)" rx="32"/>
<!-- Terminal Card -->
<g filter="url(#shadow)">
<rect x="70" y="140" width="280" height="200" rx="16" fill="#1e293b"/>
<!-- Terminal dots -->
<circle cx="95" cy="165" r="5" fill="#ef4444"/>
<circle cx="112" cy="165" r="5" fill="#f59e0b"/>
<circle cx="129" cy="165" r="5" fill="#10b981"/>
<!-- Terminal text -->
<text x="95" y="210" fill="#f8fafc" font-family="-apple-system, sans-serif" font-size="16" font-weight="700">Using Gemini-CLI</text>
<text x="95" y="235" fill="#93c5fd" font-family="-apple-system, sans-serif" font-size="14">and MCP Server</text>
<text x="95" y="260" fill="#cbd5e1" font-family="-apple-system, sans-serif" font-size="13">to Organize My</text>
<text x="95" y="285" fill="#38bdf8" font-family="-apple-system, sans-serif" font-size="15" font-weight="700">Dropbox</text>
</g>
<!-- Server Rack / MCP node -->
<g filter="url(#shadow)">
<rect x="400" y="130" width="130" height="220" rx="16" fill="#e2e8f0" stroke="#94a3b8" stroke-width="4"/>
<circle cx="465" cy="165" r="14" fill="#64748b"/>
<line x1="430" y1="210" x2="500" y2="210" stroke="#cbd5e1" stroke-width="4" stroke-linecap="round"/>
<line x1="430" y1="235" x2="500" y2="235" stroke="#cbd5e1" stroke-width="4" stroke-linecap="round"/>
<line x1="430" y1="260" x2="500" y2="260" stroke="#cbd5e1" stroke-width="4" stroke-linecap="round"/>
<line x1="430" y1="285" x2="500" y2="285" stroke="#cbd5e1" stroke-width="4" stroke-linecap="round"/>
</g>
<!-- Connecting Arrows -->
<path d="M 350 240 L 400 240" stroke="#0284c7" stroke-width="6" stroke-dasharray="6,4" stroke-linecap="round"/>
<path d="M 210 340 L 210 390 L 300 390 L 300 420" stroke="#0284c7" stroke-width="5" stroke-linecap="round"/>
<polygon points="300,435 292,418 308,418" fill="#0284c7"/>
<!-- Dropbox Box Icon -->
<g transform="translate(230, 440)" filter="url(#shadow)">
<!-- Top Left Diamond -->
<polygon points="70,10 35,32 70,55 105,32" fill="#0061ff"/>
<!-- Top Right Diamond -->
<polygon points="70,10 105,32 140,10 105,-12" fill="#3b82f6"/>
<!-- Bottom diamond front -->
<polygon points="70,55 35,78 70,100 105,78" fill="#1d4ed8"/>
<!-- Open flap left -->
<polygon points="35,32 0,55 35,78 70,55" fill="#2563eb"/>
<!-- Open flap right -->
<polygon points="105,32 70,55 105,78 140,55" fill="#1d4ed8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,45 @@
---
title: "Using Gemini-CLI and MCP server to organize my dropbox"
date: 2025-08-31
author: "haopengzhan"
category: "Tech"
tags: ["AI Era", "Dropbox", "Gemini", "LLM", "Productivity", "k8s"]
excerpt: "I think many people face the same dilemma I did: Dropbox fills up with thousands of disorganized files. Here is how I leveraged Gemini-CLI and Model Context Protocol to automate organization."
cover: images/cover.svg
lang: en
---
I think many people face the same dilemma I did: years of accumulated receipts, research papers, camera uploads, and archives scattered across folders.
When managing workloads in modern infrastructure, we treat our nodes cleanly with a streamlined **container image** deployed across clusters. But our personal storage often looks like a monolithic legacy system.
## The Problem with Cloud Storage
Cloud storage services like Dropbox offer immense space, but search and organization remain rudimentary. You often remember *concepts* rather than filenames:
- "That invoice from AWS last quarter"
- "The PDF paper on distributed consensus"
- "A screenshot of my terminal settings"
## Introducing MCP (Model Context Protocol)
The Model Context Protocol connects LLMs with local or remote toolsets. By creating an MCP server for Dropbox, we expose simple tools:
```bash
# Running the MCP server locally or in a container
npx -y @modelcontextprotocol/server-dropbox
```
And in Python:
```python
import dropbox
def list_unorganized_files(dbx, path="/Inbox"):
res = dbx.files_list_folder(path)
return [entry.name for entry in res.entries]
```
## Results and Workflow
Once configured, Gemini can inspect file headers, categorize documents into appropriate directories, and even rename messy receipts into normalized ISO dates (`YYYY-MM-DD-merchant-amount.pdf`).
@@ -0,0 +1,27 @@
---
title: "使用 Gemini-CLI 与 MCP 服务整理 Dropbox"
date: 2025-08-31
author: "haopengzhan"
category: "Tech"
tags: ["AI Era", "Dropbox", "Gemini", "LLM", "Productivity", "k8s"]
excerpt: "面对堆积如山的 Dropbox 文件,如何使用大语言模型与 Model Context Protocol 构建自动化归档助手?"
cover: images/cover.svg
lang: zh
---
我想很多人都会遇到和我一样的困境:多年累积的单据、学术论文、照片杂乱无章地存放在 Dropbox 中。
在日常管理集群基础设施时,我们通过标准化的 **container image** 保持每个节点的干净与可复现。但我们的个人存储往往像一个臃肿的单体遗留系统。
## 云存储的管理困境
传统的文件夹分类需要大量人工维护,而且往往随着时间推移逐渐失控。借助 MCP 协议,我们可以让 AI 拥有读写文件的上下文能力。
```bash
# 启动 MCP 服务
npx -y @modelcontextprotocol/server-dropbox
```
## 总结与成效
现在每天晚上脚本自动运行,未分类文件自动按年月和类别归档,大幅提升了个人数字资产的管理效率。
+110
View File
@@ -0,0 +1,110 @@
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const ejs = require('ejs');
const JS_DIRS = ['server', 'public/js', 'sample-content/plugins', 'test'];
const VIEW_DIR = 'views';
const CSS_DIR = 'public/css';
let jsFilesChecked = 0;
let ejsFilesChecked = 0;
let cssFilesChecked = 0;
let errorCount = 0;
// 1. JavaScript Checker
function checkJsFile(filePath) {
jsFilesChecked++;
const code = fs.readFileSync(filePath, 'utf8');
try {
new vm.Script(code, { filename: filePath });
} catch (err) {
console.error(`❌ Syntax Error in ${filePath}:\n ${err.message}`);
errorCount++;
return;
}
if (/\bdebugger\b/.test(code)) {
console.error(`⚠️ Warning in ${filePath}: Found 'debugger' statement.`);
errorCount++;
}
if (/-----BEGIN (RSA|EC|OPENSSH|PRIVATE) KEY-----/.test(code)) {
console.error(`🚨 Security Alert in ${filePath}: Found private key pattern!`);
errorCount++;
}
}
// 2. EJS Template Checker
function checkEjsFile(filePath) {
ejsFilesChecked++;
const content = fs.readFileSync(filePath, 'utf8');
// Verify EJS syntax compilation
try {
ejs.compile(content, { filename: filePath, client: false });
} catch (err) {
console.error(`❌ EJS Compilation Error in ${filePath}:\n ${err.message}`);
errorCount++;
return;
}
// Prevent inline style anti-patterns that break IDE CSS linters: style="... <%= ... %> ..."
const styleMatch = content.match(/style="[^"]*<[%#=][\s\S]*?"/g);
if (styleMatch) {
console.error(`❌ Linter Error in ${filePath}: Dynamic EJS expression inside style="" attribute causes IDE CSS linter errors. Use CSS utility classes instead:\n Matched: ${styleMatch[0]}`);
errorCount++;
}
}
// 3. CSS Checker (Brace matching)
function checkCssFile(filePath) {
cssFilesChecked++;
const content = fs.readFileSync(filePath, 'utf8');
let openBraces = 0;
for (let i = 0; i < content.length; i++) {
if (content[i] === '{') openBraces++;
if (content[i] === '}') openBraces--;
if (openBraces < 0) {
console.error(`❌ CSS Error in ${filePath}: Unexpected closing brace '}' at char ${i}`);
errorCount++;
return;
}
}
if (openBraces !== 0) {
console.error(`❌ CSS Error in ${filePath}: Unclosed brace '{' (mismatched by ${openBraces})`);
errorCount++;
}
}
function walkDir(dir, ext, handler) {
if (!fs.existsSync(dir)) return;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name !== 'node_modules' && entry.name !== '.git') {
walkDir(fullPath, ext, handler);
}
} else if (entry.isFile() && entry.name.endsWith(ext)) {
handler(fullPath);
}
}
}
console.log('🔍 Running comprehensive project linter (JS, EJS templates, CSS)...');
JS_DIRS.forEach(d => walkDir(d, '.js', checkJsFile));
walkDir(VIEW_DIR, '.ejs', checkEjsFile);
walkDir(CSS_DIR, '.css', checkCssFile);
if (errorCount === 0) {
console.log(`✓ All files passed verification!`);
console.log(` - ${jsFilesChecked} JavaScript files`);
console.log(` - ${ejsFilesChecked} EJS templates`);
console.log(` - ${cssFilesChecked} CSS stylesheets`);
process.exit(0);
} else {
console.error(`\n❌ Linting failed with ${errorCount} error(s).`);
process.exit(1);
}
+64
View File
@@ -0,0 +1,64 @@
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
// Content directory: prioritize ENV var, then /app/content (in K8s), fallback to ./sample-content
function resolveContentDir() {
if (process.env.CONTENT_DIR && fs.existsSync(process.env.CONTENT_DIR)) {
return path.resolve(process.env.CONTENT_DIR);
}
if (fs.existsSync('/app/content')) {
return '/app/content';
}
return path.resolve(__dirname, '../sample-content');
}
const CONTENT_DIR = resolveContentDir();
function loadSiteConfig() {
const configPath = path.join(CONTENT_DIR, 'config.yaml');
const defaults = {
site: {
title: "Pengzhan",
tagline: "Was childish, Now foolish",
url: "https://pengzhan.dev",
author: {
name: "Pengzhan Hao",
subtitle: "Software engineer on Cloud",
gravatar: ""
}
},
navigation: [
{ label: "Home", path: "/" },
{ label: "Blogs", path: "/blogs" },
{ label: "Search", path: "/search" },
{ label: "Quick Links", path: "/quick-links" }
],
social: [
{ platform: "github", url: "https://github.com/CoderSherlock" },
{ platform: "instagram", url: "https://www.instagram.com/pengzhan.hao/" },
{ platform: "linkedin", url: "https://www.linkedin.com/in/pengzhanhao/" },
{ platform: "email", address: "[email protected]" }
],
search: {
recommended: []
},
plugins: {}
};
if (fs.existsSync(configPath)) {
try {
const fileContent = fs.readFileSync(configPath, 'utf8');
const parsed = yaml.load(fileContent);
return { ...defaults, ...parsed };
} catch (err) {
console.error(`[Config] Error loading ${configPath}:`, err);
}
}
return defaults;
}
module.exports = {
CONTENT_DIR,
loadSiteConfig
};
+120
View File
@@ -0,0 +1,120 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const { CONTENT_DIR, loadSiteConfig } = require('./config');
const contentService = require('./services/content');
const commentService = require('./services/comments');
const pluginManager = require('./services/plugin-manager');
const watcherService = require('./services/watcher');
const seoMiddleware = require('./middleware/seo');
const pageCache = require('./middleware/cache');
const { notFoundHandler, serverErrorHandler } = require('./middleware/error-handler');
// Route modules
const homeRouter = require('./routes/home');
const blogsRouter = require('./routes/blogs');
const searchRouter = require('./routes/search');
const feedRouter = require('./routes/feed');
const sitemapRouter = require('./routes/sitemap');
const commentsRouter = require('./routes/comments');
const postRouter = require('./routes/post');
const pagesRouter = require('./routes/pages');
const adminRouter = require('./routes/admin');
const app = express();
const PORT = process.env.PORT || 3000;
// View engine setup
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../views'));
// Parse JSON & URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
// Load site configuration
const siteConfig = loadSiteConfig();
// Serve built-in skeleton static assets (CSS, JS, fonts)
app.use(express.static(path.join(__dirname, '../public')));
// Serve Persistent Volume static directories
const pvStaticDir = path.join(CONTENT_DIR, 'static');
if (fs.existsSync(pvStaticDir)) {
app.use(express.static(pvStaticDir));
}
// Serve PV custom CSS if present
const pvCssDir = path.join(CONTENT_DIR, 'css');
if (fs.existsSync(pvCssDir)) {
app.use('/css/custom', express.static(pvCssDir));
}
// Serve post assets (e.g. cover images) at /posts-assets/<slug>/...
const pvPostsDir = path.join(CONTENT_DIR, 'posts');
if (fs.existsSync(pvPostsDir)) {
app.use('/posts-assets', express.static(pvPostsDir));
}
// Initialize services
commentService.init(CONTENT_DIR);
pluginManager.loadPlugins(CONTENT_DIR, app, siteConfig);
contentService.init(CONTENT_DIR);
watcherService.start(CONTENT_DIR, app);
// Inject SEO locals & plugin hook helper
app.use(seoMiddleware(siteConfig));
app.use((req, res, next) => {
res.locals.pluginHook = (hookName, extraContext = {}) => {
return pluginManager.renderHook(hookName, {
req,
res,
site: res.locals.site,
...extraContext
});
};
next();
});
// Super Page Cache micro-caching
app.use(pageCache.middleware());
// Mount routes in order of specificity
app.use('/', adminRouter);
app.use('/', homeRouter);
app.use('/', blogsRouter);
app.use('/', searchRouter);
app.use('/', feedRouter);
app.use('/', sitemapRouter);
app.use('/', commentsRouter);
// Specific blog post routes (/:slug, /:slug/:lang)
app.use('/', postRouter);
// Fallback standalone pages (/:pageSlug from pages/)
app.use('/', pagesRouter);
// Error handlers
app.use(notFoundHandler);
app.use(serverErrorHandler);
if (require.main === module) {
const server = app.listen(PORT, () => {
console.log(`=========================================`);
console.log(` Pengzhan.dev Web Server running at:`);
console.log(` http://localhost:${PORT}`);
console.log(` Content directory: ${CONTENT_DIR}`);
console.log(`=========================================`);
});
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
watcherService.close();
server.close(() => {
console.log('HTTP server closed');
});
});
}
module.exports = app;
+70
View File
@@ -0,0 +1,70 @@
const crypto = require('crypto');
class SuperPageCache {
constructor() {
this.cache = new Map(); // path -> { body, contentType, etag, createdAt }
this.ttlMs = 10 * 60 * 1000; // 10 minutes default memory cache
}
middleware() {
return (req, res, next) => {
// Only cache GET requests that are not admin, guanli, or API routes
if (req.method !== 'GET' || req.path.startsWith('/guanli') || req.path.startsWith('/admin') || req.path.startsWith('/api')) {
return next();
}
const cacheKey = req.originalUrl || req.url;
const cached = this.cache.get(cacheKey);
if (cached && (Date.now() - cached.createdAt < this.ttlMs)) {
// Check conditional ETag
if (req.headers['if-none-match'] === cached.etag) {
res.setHeader('X-Cache', 'HIT-304');
return res.status(304).end();
}
res.setHeader('X-Cache', 'HIT');
res.setHeader('ETag', cached.etag);
res.setHeader('Content-Type', cached.contentType || 'text/html; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
return res.send(cached.body);
}
// Intercept res.send to populate cache
const originalSend = res.send.bind(res);
res.send = (body) => {
if (res.statusCode === 200 && typeof body === 'string') {
const etag = `W/"${crypto.createHash('md5').update(body).digest('hex').slice(0, 16)}"`;
this.cache.set(cacheKey, {
body,
contentType: res.getHeader('Content-Type') || 'text/html; charset=utf-8',
etag,
createdAt: Date.now()
});
res.setHeader('X-Cache', 'MISS');
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
}
return originalSend(body);
};
next();
};
}
clear() {
const size = this.cache.size;
this.cache.clear();
console.log(`[SuperPageCache] Flushed ${size} cached pages.`);
}
invalidate(urlPattern) {
for (const key of this.cache.keys()) {
if (key.includes(urlPattern)) {
this.cache.delete(key);
}
}
}
}
module.exports = new SuperPageCache();
+23
View File
@@ -0,0 +1,23 @@
function notFoundHandler(req, res, next) {
res.status(404);
res.render('pages/error', {
title: '404 - Page Not Found',
errorCode: 404,
errorMessage: "The page you are looking for doesn't exist or has moved."
});
}
function serverErrorHandler(err, req, res, next) {
console.error('[ServerError]', err);
res.status(500);
res.render('pages/error', {
title: '500 - Server Error',
errorCode: 500,
errorMessage: "An unexpected error occurred."
});
}
module.exports = {
notFoundHandler,
serverErrorHandler
};
+106
View File
@@ -0,0 +1,106 @@
function generateJsonLd(site, seo, req) {
const siteUrl = site.url || 'https://pengzhan.dev';
const person = {
'@type': 'Person',
'@id': `${siteUrl}/#person`,
name: (site.author && site.author.name) || site.title,
url: siteUrl,
jobTitle: (site.author && site.author.subtitle) || 'Software Engineer',
sameAs: (site.social || []).map(s => s.url).filter(Boolean)
};
const website = {
'@type': 'WebSite',
'@id': `${siteUrl}/#website`,
url: siteUrl,
name: site.title,
description: site.tagline,
publisher: { '@id': `${siteUrl}/#person` },
potentialAction: [
{
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${siteUrl}/search?q={search_term_string}`
},
'query-input': 'required name=search_term_string'
}
]
};
const graph = [person, website];
if (seo.type === 'article') {
const blogPosting = {
'@type': 'BlogPosting',
'@id': `${seo.canonicalUrl}#article`,
isPartOf: { '@id': `${siteUrl}/#website` },
author: { '@id': `${siteUrl}/#person` },
headline: seo.title,
datePublished: seo.publishedTime || new Date().toISOString(),
dateModified: seo.modifiedTime || seo.publishedTime || new Date().toISOString(),
mainEntityOfPage: seo.canonicalUrl,
description: seo.description,
image: seo.ogImage,
articleSection: seo.category || 'Tech',
keywords: Array.isArray(seo.tags) ? seo.tags.join(', ') : seo.tags
};
graph.push(blogPosting);
}
// Breadcrumbs
const breadcrumbItems = [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: siteUrl
}
];
if (req.path !== '/') {
breadcrumbItems.push({
'@type': 'ListItem',
position: 2,
name: seo.title,
item: seo.canonicalUrl
});
}
graph.push({
'@type': 'BreadcrumbList',
'@id': `${seo.canonicalUrl}#breadcrumb`,
itemListElement: breadcrumbItems
});
return JSON.stringify({
'@context': 'https://schema.org',
'@graph': graph
});
}
function seoMiddleware(siteConfig) {
return (req, res, next) => {
res.locals.site = siteConfig.site;
res.locals.navigation = siteConfig.navigation;
res.locals.social = siteConfig.social;
res.locals.currentPath = req.path;
// Helper to generate SEO metadata
res.locals.seo = {
title: siteConfig.site.title,
description: siteConfig.site.tagline,
canonicalUrl: `${siteConfig.site.url}${req.path}`,
ogImage: `${siteConfig.site.url}/images/og-default.png`,
type: 'website',
tags: [],
category: ''
};
res.locals.getJsonLd = () => generateJsonLd(siteConfig.site, res.locals.seo, req);
next();
};
}
module.exports = seoMiddleware;
+262
View File
@@ -0,0 +1,262 @@
const express = require('express');
const router = express.Router();
const commentService = require('../services/comments');
const contentService = require('../services/content');
const pageCache = require('../middleware/cache');
const { loadSiteConfig } = require('../config');
// Helper to parse cookies from headers without requiring extra npm dependencies
function parseCookies(req) {
const list = {};
const rc = req.headers.cookie;
if (rc) {
rc.split(';').forEach(cookie => {
const parts = cookie.split('=');
const key = parts.shift().trim();
const val = parts.join('=').trim();
try {
list[key] = decodeURIComponent(val);
} catch (_) {
list[key] = val;
}
});
}
return list;
}
function getExpectedAdminKey() {
const config = loadSiteConfig();
return process.env.ADMIN_SECRET || config.admin?.secret || config.site?.admin_secret || 'admin';
}
// Admin authentication middleware
function requireAdminAuth(req, res, next) {
const expectedKey = getExpectedAdminKey();
const cookies = parseCookies(req);
const providedKey = req.query.key || req.headers['x-admin-key'] || cookies.admin_key;
if (providedKey && providedKey === expectedKey) {
// If authenticated via query param, set cookie for convenience
if (req.query.key) {
res.setHeader('Set-Cookie', `admin_key=${encodeURIComponent(expectedKey)}; Path=/; HttpOnly; SameSite=Lax`);
}
return next();
}
// Render clean login prompt if unauthorized
res.status(401);
return res.render('pages/admin-login', {
title: 'Admin Authorization',
error: providedKey ? 'Incorrect admin password.' : null,
redirect: req.originalUrl
});
}
// Explicit 404 for legacy /admin routes to prevent leakage and bust browser 301 cache
router.all(['/admin', '/admin/*'], (req, res) => {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
res.status(404).render('pages/error', {
title: '404 - Page Not Found',
errorCode: 404,
errorMessage: "The page you are looking for doesn't exist or has moved."
});
});
// Redirect /guanli root to comments
router.get('/guanli', (req, res) => {
res.redirect('/guanli/comments');
});
// Login GET endpoint (clean access)
router.get('/guanli/login', (req, res) => {
res.render('pages/admin-login', {
title: 'Admin Authorization',
error: null,
redirect: req.query.redirect || '/guanli/comments'
});
});
// Login POST endpoint
router.post('/guanli/login', (req, res) => {
const expectedKey = getExpectedAdminKey();
const providedKey = (req.body.key || '').trim();
const redirectUrl = req.body.redirect || '/guanli/comments';
if (providedKey === expectedKey) {
res.setHeader('Set-Cookie', `admin_key=${encodeURIComponent(expectedKey)}; Path=/; HttpOnly; SameSite=Lax`);
return res.redirect(redirectUrl);
}
res.status(401).render('pages/admin-login', {
title: 'Admin Authorization',
error: 'Incorrect admin password. Please try again.',
redirect: redirectUrl
});
});
// Logout endpoint
router.get('/guanli/logout', (req, res) => {
res.setHeader('Set-Cookie', 'admin_key=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT');
res.redirect('/guanli/comments');
});
const dbService = require('../services/db');
// 1. Comment Moderation Dashboard
router.get('/guanli/comments', requireAdminAuth, (req, res) => {
const status = req.query.status || 'all';
const q = req.query.q || '';
const page = parseInt(req.query.page, 10) || 1;
const limit = 15;
const paginated = dbService.getPaginatedComments({ status, q, page, limit });
const counts = dbService.getCommentCounts();
const adminKey = getExpectedAdminKey();
const stats = dbService.getAdminStats();
res.render('pages/admin-comments', {
title: 'Comment Moderation Dashboard',
comments: paginated.comments,
totalComments: paginated.total,
page: paginated.page,
totalPages: paginated.totalPages,
status,
q,
counts,
adminKey,
stats
});
});
// 2a. Single Moderation Action Endpoint
router.post('/guanli/comments/:action/:id', requireAdminAuth, (req, res) => {
const { action, id } = req.params;
if (action === 'approve') {
commentService.approveComment(Number(id));
pageCache.clear();
} else if (action === 'reject' || action === 'spam') {
commentService.rejectComment(Number(id));
pageCache.clear();
} else if (action === 'delete') {
commentService.deleteComment(Number(id));
pageCache.clear();
}
const isAjax = req.xhr || req.headers['content-type'] === 'application/json' || req.headers.accept?.includes('application/json');
if (isAjax) {
const counts = dbService.getCommentCounts();
const stats = dbService.getAdminStats();
return res.json({ success: true, action, id: Number(id), counts, stats });
}
const returnUrl = req.headers.referer || '/guanli/comments';
res.redirect(returnUrl);
});
// 2b. Batch Moderation Action Endpoint
router.post('/guanli/comments/batch', requireAdminAuth, (req, res) => {
const action = req.body.action;
let ids = req.body.ids;
if (typeof ids === 'string') {
ids = ids.split(',').map(s => Number(s.trim())).filter(Boolean);
} else if (Array.isArray(ids)) {
ids = ids.map(Number).filter(Boolean);
} else {
ids = [];
}
if (ids.length > 0) {
if (action === 'approve') {
commentService.batchApproveComments(ids);
pageCache.clear();
} else if (action === 'reject' || action === 'spam') {
commentService.batchRejectComments(ids);
pageCache.clear();
} else if (action === 'delete') {
commentService.batchDeleteComments(ids);
pageCache.clear();
}
}
const isAjax = req.xhr || req.headers['content-type'] === 'application/json' || req.headers.accept?.includes('application/json');
if (isAjax) {
const counts = dbService.getCommentCounts();
const stats = dbService.getAdminStats();
return res.json({ success: true, action, ids, count: ids.length, counts, stats });
}
const returnUrl = req.headers.referer || '/guanli/comments';
res.redirect(returnUrl);
});
// 3. Dynamic SEO Analyzer Dashboard
router.get('/guanli/seo', requireAdminAuth, (req, res) => {
const posts = contentService.getAllPosts();
const adminKey = getExpectedAdminKey();
const stats = dbService.getAdminStats();
const report = [];
for (const post of posts) {
const raw = post.rawContent || '';
const internalLinks = (raw.match(/\[[^\]]+\]\((\/[^)]+)\)/g) || []).length;
const externalLinks = (raw.match(/\[[^\]]+\]\((https?:\/\/[^)]+)\)/g) || []).length;
const imagesMissingAlt = (raw.match(/!\[\s*\]\([^)]+\)/g) || []).length;
const totalImages = (raw.match(/!\[[^\]]*\]\([^)]+\)/g) || []).length;
// Check heading hierarchy
const headings = post.toc || [];
let headingIssues = [];
let lastLevel = 1;
for (const h of headings) {
if (h.level > lastLevel + 1) {
headingIssues.push(`Skipped heading level: from h${lastLevel} to h${h.level} ("${h.title}")`);
}
lastLevel = h.level;
}
const wordCount = raw.trim().split(/\s+/).length;
const titleLength = post.title.length;
report.push({
type: 'post',
slug: post.slug,
title: post.title,
wordCount,
titleLength,
titleOk: titleLength >= 20 && titleLength <= 70,
internalLinks,
externalLinks,
totalImages,
imagesMissingAlt,
headingIssues,
hasExcerpt: Boolean(post.excerpt && post.excerpt.length > 50)
});
}
res.render('pages/admin-seo', {
title: 'SEO Analyzer & Content Audit',
report,
adminKey,
stats
});
});
// 4. Search Analytics Dashboard
router.get('/guanli/search', requireAdminAuth, (req, res) => {
const adminKey = getExpectedAdminKey();
const stats = dbService.getAdminStats();
const searchLogs = dbService.getAllSearchLogs(100);
const recommendations = dbService.getTopSearchRecommendations(20);
res.render('pages/admin-search', {
title: 'Search Analytics & Query Logs',
adminKey,
stats,
searchLogs,
recommendations
});
});
module.exports = router;
+42
View File
@@ -0,0 +1,42 @@
const express = require('express');
const router = express.Router();
const contentService = require('../services/content');
router.get('/blogs', (req, res) => {
let posts = contentService.getAllPosts();
const { category, tag, page } = req.query;
let activeFilter = null;
if (category) {
const catLower = category.trim().toLowerCase();
posts = posts.filter(p => (p.category || '').toLowerCase() === catLower);
activeFilter = { type: 'category', value: category };
} else if (tag) {
const tagLower = tag.trim().toLowerCase();
posts = posts.filter(p => (p.tags || []).some(t => t.toLowerCase() === tagLower));
activeFilter = { type: 'tag', value: tag };
}
const postsPerPage = 12;
const currentPage = Math.max(1, parseInt(page, 10) || 1);
const totalPosts = posts.length;
const totalPages = Math.ceil(totalPosts / postsPerPage);
const startIndex = (currentPage - 1) * postsPerPage;
const paginatedPosts = posts.slice(startIndex, startIndex + postsPerPage);
const taxonomy = contentService.getTaxonomy();
res.render('pages/blogs', {
title: activeFilter ? `Blogs - ${activeFilter.type}: ${activeFilter.value}` : 'Blogs',
posts: paginatedPosts,
currentPage,
totalPages,
totalPosts,
activeFilter,
taxonomy
});
});
module.exports = router;
+60
View File
@@ -0,0 +1,60 @@
const express = require('express');
const router = express.Router();
const commentService = require('../services/comments');
const contentService = require('../services/content');
const pluginManager = require('../services/plugin-manager');
router.get('/api/comments/:slug', (req, res) => {
const comments = commentService.getCommentsForPost(req.params.slug);
res.json({ comments });
});
router.post('/api/comments', async (req, res) => {
try {
const { postSlug, authorName, authorEmail, authorWebsite, body, honeypot } = req.body;
if (!postSlug || !authorName || !body) {
return res.status(400).json({ error: 'Missing required fields' });
}
const clientIp = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const newComment = commentService.addComment({
postSlug,
authorName,
authorEmail,
authorWebsite,
body,
honeypot,
ip: clientIp
});
// Invalidate page cache so new comment and admin views update immediately
const pageCache = require('../middleware/cache');
pageCache.clear();
const post = contentService.getPost(postSlug);
// Fire plugin lifecycle hook asynchronously
pluginManager.triggerCommentCreated(newComment, post).catch(err => {
console.error('[CommentRoute] Error in onCommentCreated plugin hook:', err);
});
// If submitted via AJAX, return JSON
if (req.xhr || req.headers.accept.includes('json')) {
return res.json({
success: true,
comment: newComment,
message: newComment.approved === 1 ? 'Comment posted!' : 'Your comment is awaiting moderation.'
});
}
// Otherwise redirect back to post comments anchor with flash message
const msg = newComment.approved === 1 ? 'comment_posted' : 'comment_pending';
res.redirect(`/${postSlug}?comment_status=${msg}#comments`);
} catch (err) {
console.error('[CommentRoute] Error saving comment:', err);
res.status(500).json({ error: 'Failed to post comment' });
}
});
module.exports = router;
+58
View File
@@ -0,0 +1,58 @@
const express = require('express');
const router = express.Router();
const { Feed } = require('feed');
const contentService = require('../services/content');
function generateFeed(site) {
const feed = new Feed({
title: site.title,
description: site.tagline,
id: site.url,
link: site.url,
language: 'en',
image: `${site.url}/images/og-default.png`,
favicon: `${site.url}/favicon.ico`,
copyright: `All rights reserved ${new Date().getFullYear()}, ${site.author ? site.author.name : site.title}`,
author: {
name: site.author ? site.author.name : site.title,
link: site.url
}
});
const posts = contentService.getAllPosts();
for (const post of posts) {
const postUrl = `${site.url}/${post.slug}`;
feed.addItem({
title: post.title,
id: postUrl,
link: postUrl,
description: post.excerpt,
content: post.contentHtml,
author: [
{
name: post.author,
link: site.url
}
],
date: post.date,
image: post.cover ? (post.cover.startsWith('http') ? post.cover : `${site.url}${post.cover}`) : undefined
});
}
return feed;
}
router.get(['/feed', '/rss.xml', '/feed.xml'], (req, res) => {
const feed = generateFeed(res.locals.site);
res.header('Content-Type', 'application/xml; charset=utf-8');
res.send(feed.rss2());
});
router.get('/atom.xml', (req, res) => {
const feed = generateFeed(res.locals.site);
res.header('Content-Type', 'application/atom+xml; charset=utf-8');
res.send(feed.atom1());
});
module.exports = router;
+16
View File
@@ -0,0 +1,16 @@
const express = require('express');
const router = express.Router();
const contentService = require('../services/content');
router.get('/', (req, res) => {
const homePage = contentService.getPage('/');
const recentPosts = contentService.getAllPosts().slice(0, 6);
res.render('pages/home', {
title: homePage ? homePage.title : res.locals.site.title,
page: homePage,
recentPosts
});
});
module.exports = router;
+28
View File
@@ -0,0 +1,28 @@
const express = require('express');
const router = express.Router();
const contentService = require('../services/content');
router.get('/:slug', (req, res, next) => {
const pagePath = `/${req.params.slug}`;
const page = contentService.getPage(pagePath);
if (!page) {
return next(); // Fall through to 404
}
res.locals.seo = {
title: `${page.title} - ${res.locals.site.title}`,
description: page.frontmatter.description || res.locals.site.tagline,
canonicalUrl: `${res.locals.site.url}${pagePath}`,
ogImage: res.locals.seo.ogImage,
type: 'website'
};
const layout = page.layout === 'home' ? 'pages/home' : 'pages/page';
res.render(layout, {
title: page.title,
page
});
});
module.exports = router;
+52
View File
@@ -0,0 +1,52 @@
const express = require('express');
const router = express.Router();
const contentService = require('../services/content');
const commentService = require('../services/comments');
function renderPost(req, res, next, slug, lang) {
const post = contentService.getPost(slug, lang);
if (!post) {
return next(); // Fall through to standalone pages or 404
}
const { prev, next: nextPost } = contentService.getAdjacentPosts(slug);
const comments = commentService.getCommentsForPost(slug);
// Set SEO metadata for post
res.locals.seo = {
title: `${post.title} - ${res.locals.site.title}`,
description: post.excerpt || res.locals.site.tagline,
canonicalUrl: `${res.locals.site.url}/${post.slug}${post.lang !== 'en' && !post.isDefault ? `/${post.lang}` : ''}`,
ogImage: post.cover ? (post.cover.startsWith('http') ? post.cover : `${res.locals.site.url}${post.cover}`) : res.locals.seo.ogImage,
type: 'article',
publishedTime: post.date.toISOString(),
author: post.author
};
res.render('pages/post', {
title: post.title,
post,
currentLang: lang || post.lang,
comments,
commentStatus: req.query.comment_status,
prevPost: prev,
nextPost
});
}
// Route for bilingual variant: /<post-slug>/<lang> (e.g. /my-post/zh)
router.get('/:slug/:lang([a-zA-Z]{2,5})', (req, res, next) => {
renderPost(req, res, next, req.params.slug, req.params.lang);
});
// Route for default language: /<post-slug>
router.get('/:slug', (req, res, next) => {
// Exclude reserved top-level prefixes if any
const reserved = ['blogs', 'search', 'quick-links', 'feed', 'rss.xml', 'sitemap.xml', 'robots.txt', 'api', 'plugins', 'public', 'posts-assets', 'guanli', 'admin'];
if (reserved.includes(req.params.slug)) {
return next();
}
renderPost(req, res, next, req.params.slug, null);
});
module.exports = router;
+104
View File
@@ -0,0 +1,104 @@
const express = require('express');
const router = express.Router();
const searchEngine = require('../services/search-engine');
const contentService = require('../services/content');
const { loadSiteConfig } = require('../config');
function getRecommendedSearches() {
const config = loadSiteConfig();
const taxonomy = contentService.getTaxonomy();
const dbService = require('../services/db');
const dynamicPills = [];
// 1. Add categories as discovery pills
for (const cat of taxonomy.categories) {
dynamicPills.push({
label: `category:${cat.name.toLowerCase()}`,
query: `category:${cat.name.toLowerCase()}`,
count: cat.count
});
}
// 2. Add top tags as discovery pills
for (const tag of taxonomy.tags.slice(0, 6)) {
dynamicPills.push({
label: `tag:${tag.name.toLowerCase()}`,
query: `tag:${tag.name.toLowerCase()}`,
count: tag.count
});
}
// 3. Add top trending searches from SQLite database
const trendingDb = dbService.getTopSearchRecommendations(4);
const customPills = [];
for (const item of trendingDb) {
if (!dynamicPills.some(p => p.query === item.query)) {
customPills.push({
label: item.label,
query: item.query
});
}
}
// 4. Add any static curated pills from config
const staticConfig = (config.search && config.search.recommended) || [];
for (const item of staticConfig) {
if (!dynamicPills.some(p => p.query === item.query) && !customPills.some(p => p.query === item.query)) {
customPills.push(item);
}
}
return {
dynamicPills,
customPills
};
}
router.get('/search', (req, res) => {
const query = (req.query.q || '').trim();
const recommended = getRecommendedSearches();
let searchData = {
query: { filters: {}, phrases: [], keywords: [], raw: '' },
results: [],
total: 0
};
if (query) {
searchData = searchEngine.search(query);
}
res.render('pages/search', {
title: query ? `Search: ${query}` : 'Search',
query,
searchData,
recommended
});
});
// JSON API endpoint for client-side search autocomplete / live search
router.get('/api/search', (req, res) => {
const query = (req.query.q || '').trim();
if (!query) {
return res.json({ query: null, results: [], total: 0 });
}
const searchData = searchEngine.search(query);
res.json({
query: searchData.query,
total: searchData.total,
results: searchData.results.map(r => ({
slug: r.post.slug,
lang: r.post.lang,
title: r.post.title,
date: r.post.date,
category: r.post.category,
tags: r.post.tags,
score: r.score,
snippet: r.snippet
}))
});
});
module.exports = router;
+75
View File
@@ -0,0 +1,75 @@
const express = require('express');
const router = express.Router();
const contentService = require('../services/content');
router.get('/sitemap.xml', (req, res) => {
const siteUrl = res.locals.site.url;
const posts = contentService.getAllPosts();
const pages = Array.from(contentService.pagesBySlug.values());
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
// Static / Top-level pages
const staticRoutes = ['/', '/blogs', '/search'];
for (const route of staticRoutes) {
xml += ' <url>\n';
xml += ` <loc>${siteUrl}${route}</loc>\n`;
xml += ` <changefreq>daily</changefreq>\n`;
xml += ` <priority>${route === '/' ? '1.0' : '0.8'}</priority>\n`;
xml += ' </url>\n';
}
// Standalone pages
for (const page of pages) {
if (!page.slug || page.slug === '/') continue;
const cleanSlug = page.slug.startsWith('/') ? page.slug : `/${page.slug}`;
if (cleanSlug === '/guanli' || cleanSlug.startsWith('/guanli/') || cleanSlug === '/admin' || cleanSlug.startsWith('/admin/')) {
continue;
}
xml += ' <url>\n';
xml += ` <loc>${siteUrl}${cleanSlug}</loc>\n`;
xml += ` <changefreq>weekly</changefreq>\n`;
xml += ` <priority>0.7</priority>\n`;
xml += ' </url>\n';
}
// Blog posts (including translations)
for (const post of posts) {
if (!post.slug || post.slug.startsWith('guanli') || post.slug.startsWith('admin')) continue;
xml += ' <url>\n';
xml += ` <loc>${siteUrl}/${post.slug}</loc>\n`;
xml += ` <lastmod>${post.date.toISOString().split('T')[0]}</lastmod>\n`;
xml += ` <changefreq>monthly</changefreq>\n`;
xml += ` <priority>0.9</priority>\n`;
xml += ' </url>\n';
// Bilingual URLs
if (post.availableLanguages) {
for (const lang of post.availableLanguages) {
if (lang !== post.lang && lang !== 'default') {
xml += ' <url>\n';
xml += ` <loc>${siteUrl}/${post.slug}/${lang}</loc>\n`;
xml += ` <lastmod>${post.date.toISOString().split('T')[0]}</lastmod>\n`;
xml += ` <changefreq>monthly</changefreq>\n`;
xml += ` <priority>0.8</priority>\n`;
xml += ' </url>\n';
}
}
}
}
xml += '</urlset>';
res.header('Content-Type', 'application/xml; charset=utf-8');
res.send(xml);
});
router.get('/robots.txt', (req, res) => {
const siteUrl = res.locals.site.url;
const content = `User-agent: *\nDisallow: /admin/\nAllow: /\n\nSitemap: ${siteUrl}/sitemap.xml\n`;
res.header('Content-Type', 'text/plain; charset=utf-8');
res.send(content);
});
module.exports = router;

Some files were not shown because too many files have changed in this diff Show More