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
@@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
BUNDLE_PATH: "_vendor/bundle"
|
|
||||||
BUNDLE_DISABLE_SHARED_GEMS: "true"
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
.git
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
.git/
|
.git/
|
||||||
.gitignore
|
.agent/
|
||||||
_site
|
sample-content/
|
||||||
.sass-cache
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
_vendor
|
*.log
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,4 +1,18 @@
|
|||||||
_site
|
# Dependencies
|
||||||
.sass-cache
|
node_modules/
|
||||||
|
|
||||||
|
# OS and Environment
|
||||||
.DS_Store
|
.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*
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
language: ruby
|
|
||||||
|
|
||||||
rvm:
|
|
||||||
- 2.5.0
|
|
||||||
|
|
||||||
script: bundle exec jekyll build
|
|
||||||
|
|
||||||
install: bundle install
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
|
|
||||||
@@ -1,66 +1,246 @@
|
|||||||
# Resume template
|
# pengzhan.dev — Dynamic Web Framework & Personal Site Skeleton
|
||||||
|
|
||||||
*A simple Jekyll + GitHub Pages powered resume template.*
|
A custom, ultra-lightweight dynamic web framework designed to replicate and replace WordPress for **pengzhan.dev**, optimized for containerized deployment on Kubernetes.
|
||||||
|
|
||||||

|
---
|
||||||
|
|
||||||
## Docs
|
## 1. Architectural Model: Skeleton vs. Persistent Volume
|
||||||
|
|
||||||
### Running locally
|
The architecture separates the **web runtime container (the "Skeleton")** from your **content and extensions (the "Persistent Volume")**:
|
||||||
|
|
||||||
To test locally, run the following in your terminal:
|
```plaintext
|
||||||
|
Kubernetes Pod
|
||||||
|
┌───────────────────────────────────────────────┐
|
||||||
|
│ Docker Container (The "Skeleton") │
|
||||||
|
│ - Node.js 22 + Express │
|
||||||
|
│ - SSR EJS Templates │
|
||||||
|
│ - Markdown-it + PrismJS (Okaidia) │
|
||||||
|
│ - Structured Schema Search Engine │
|
||||||
|
│ - Dynamic Plugin Loader │
|
||||||
|
│ - Hot-reload Watcher (Chokidar) │
|
||||||
|
└───────────────────────┬───────────────────────┘
|
||||||
|
│ mounts /app/content
|
||||||
|
▼
|
||||||
|
┌───────────────────────────────────────────────┐
|
||||||
|
│ Persistent Volume (Your Content & Data) │
|
||||||
|
│ - posts/ (Blog articles & bilinguals) │
|
||||||
|
│ - pages/ (Standalone timeless pages) │
|
||||||
|
│ - plugins/ (Custom executable plugins) │
|
||||||
|
│ - static/ (Fonts, PDFs, Images) │
|
||||||
|
│ - css/ (custom.css overrides) │
|
||||||
|
│ - data/ (site.db SQLite) │
|
||||||
|
│ - config.yaml (Site metadata & settings) │
|
||||||
|
└───────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
1. Clone repo locally
|
- **Zero Container Restarts**: Editing markdown files, uploading PDFs/images, adding new pages, or changing `config.yaml` immediately hot-reloads in memory via `chokidar`.
|
||||||
1. `bundle install`
|
- **Tiny Footprint**: Multi-stage Docker image weighs ~25MB (no React/Vue hydration overhead, no headless browser, no heavy DB daemon).
|
||||||
2. `bundle exec jekyll serve`
|
|
||||||
3. Open your browser to `localhost:4000`
|
|
||||||
|
|
||||||
### 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 .`
|
```plaintext
|
||||||
2. `docker run --rm --name resume-template -v "$PWD":/home/app --network host resume-template`
|
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
|
// 3. Transform or augment post metadata
|
||||||
2. Fork the project, make changes, and submit a pull request
|
onPostLoad({ post, pluginConfig }) {},
|
||||||
|
|
||||||
## License
|
// 4. Inject into HTML head or body
|
||||||
|
injectHead({ req, pageType, post }) {
|
||||||
|
return `<script src="..."></script>`;
|
||||||
|
},
|
||||||
|
|
||||||
The code and styles are licensed under the MIT license. [See project license.](LICENSE) Obviously you should not use the content of this demo repo in your own resume. :wink:
|
injectPostEnd({ post }) {
|
||||||
|
return `<div>Custom author bio or reaction buttons</div>`;
|
||||||
|
},
|
||||||
|
|
||||||
Disclaimer: Use of Lisa M. Simpson image and name used under [Fair Use](https://en.wikipedia.org/wiki/Fair_use) for educational purposes. Project license does not apply to use of this material.
|
// 5. Lifecycle event: new comment posted
|
||||||
|
async onCommentCreated({ comment, post }) {}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Bilingual Post Authoring
|
||||||
|
|
||||||
|
In `/app/content/posts/<post-slug>/`:
|
||||||
|
|
||||||
|
### Default Language (`index.md`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: "Using Gemini-CLI and MCP server to organize my dropbox"
|
||||||
|
date: 2025-08-31
|
||||||
|
author: "haopengzhan"
|
||||||
|
category: "Tech"
|
||||||
|
tags: ["AI Era", "Dropbox", "Gemini", "k8s"]
|
||||||
|
excerpt: "How I automated Dropbox organization with MCP."
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
|
|
||||||
|
Post content in English...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Translation Variant (`zh.md`)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: "使用 Gemini-CLI 与 MCP 服务整理 Dropbox"
|
||||||
|
lang: zh
|
||||||
|
---
|
||||||
|
|
||||||
|
Post content in Chinese...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
|
||||||
|
- `https://pengzhan.dev/<post-slug>` → Serves default version (`index.md`).
|
||||||
|
- `https://pengzhan.dev/<post-slug>/zh` → Serves Chinese version (`zh.md`).
|
||||||
|
- Both are linked via the in-page language switcher pill (`🌐 EN | 中文`).
|
||||||
|
- Only **one** entry appears in `/blogs` and `/search`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Development & Deployment
|
||||||
|
|
||||||
|
### Run Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 2. Run automated test suite (6/6 suites)
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# 3. Start local development server (uses ./sample-content by default)
|
||||||
|
npm start
|
||||||
|
# Server runs at http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t pengzhan-web:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Container with Mounted Persistent Volume
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-p 3000:3000 \
|
||||||
|
-v /path/to/my-content:/app/content \
|
||||||
|
--name pengzhan-site \
|
||||||
|
pengzhan-web:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes Deployment Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: pengzhan-web
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: web
|
||||||
|
image: pengzhan-web:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 3000
|
||||||
|
volumeMounts:
|
||||||
|
- name: content
|
||||||
|
mountPath: /app/content
|
||||||
|
volumes:
|
||||||
|
- name: content
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: pengzhan-content-pvc
|
||||||
|
```
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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']
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Associations
|
|
||||||
# comment out the urls if you do not wish to display them
|
|
||||||
|
|
||||||
- organization: PETA
|
|
||||||
role: Volunteer
|
|
||||||
year: 2001 — 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 — 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.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Degree
|
|
||||||
- degree: Doctorate of Science in Computer Science
|
|
||||||
uni: Binghamton university, the State University of New York
|
|
||||||
year: 2017 —
|
|
||||||
|
|
||||||
- degree: Master of Science in Computer Science
|
|
||||||
uni: Binghamton university, the State University of New York
|
|
||||||
year: 2015 — 2017
|
|
||||||
summary: GPA 4.0/4.0
|
|
||||||
|
|
||||||
- degree: Bachelor of Engineering in Software Engineering
|
|
||||||
uni: Beihang University
|
|
||||||
year: 2011 — 2015
|
|
||||||
summary: GPA 3.25/4.0
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Jobs
|
|
||||||
# Google
|
|
||||||
- company: Google
|
|
||||||
position: Software Engineer
|
|
||||||
duration: Jun, 2022 —
|
|
||||||
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 —
|
|
||||||
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 — 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>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Interests
|
|
||||||
- description: Human rights and world affairs
|
|
||||||
- description: Music (baritone saxiphonist)
|
|
||||||
- description: Linguistics
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# links
|
|
||||||
- url: "https://blog.pengzhan.dev/"
|
|
||||||
description: My personal blog
|
|
||||||
@@ -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 — 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 — 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 — 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).
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 %}
|
|
||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 %}
|
|
||||||
@@ -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 }} • {{ 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 }} • {{ 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 }} • {{ 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 }} • {{ 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 }} • {{ 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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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";
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 551 KiB |
|
Before Width: | Height: | Size: 72 KiB |
@@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
layout: resume
|
|
||||||
---
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/* Custom CSS overrides on Persistent Volume */
|
||||||
|
/* Any CSS written here will be loaded after base and components CSS */
|
||||||
@@ -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 & 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 • The Sixth ACM/IEEE Symposium on Edge Computing (ACM/IEEE SEC) • 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 • The 37th IEEE International Conference on Computer Communications (IEEE INFOCOM) • 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 • The 2nd ACM/IEEE Symposium on Edge Computing (ACM/IEEE SEC) • 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 • The 15th ACM International Conference on Mobile Systems, Applications, and Services (ACM MobiSys) • 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 • The 15th ACM International Conference on Mobile Systems, Applications, and Services (ACM MobiSys) • 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 – 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 • 2017, 2021</span>
|
||||||
|
</div>
|
||||||
|
<div class="recog-item">
|
||||||
|
<strong>ACM MobiSys 2017 student travel grant</strong>
|
||||||
|
<span class="recog-meta">ACM • 2017</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -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) — open-source projects, personal tools, and experiments.
|
||||||
|
- **LinkedIn**: [linkedin.com/in/pengzhanhao](https://www.linkedin.com/in/pengzhanhao/) — professional background.
|
||||||
|
- **Instagram**: [@pengzhan.hao](https://www.instagram.com/pengzhan.hao/) — 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 • ${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"><></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 & 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 总结与成效
|
||||||
|
|
||||||
|
现在每天晚上脚本自动运行,未分类文件自动按年月和类别归档,大幅提升了个人数字资产的管理效率。
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||