feat: initial release of pengzhan.dev personal editorial blog platform
- Editorial web platform built with Node.js, Express, EJS, and Vanilla CSS - Markdown content engine with YAML frontmatter, bilingual variants, and hot-reload watcher - Unified SQLite with FTS5 search indexing, query logging, and schema filters - Centered editorial post layout with margin-docked left TOC and mobile bottom sheet - WordPress-grade comment system with moderation dashboard and bulk operations under /guanli - Complete SEO suite (JSON-LD structured data, RSS/Atom feeds, XML sitemap, robots.txt) - CI/CD workflows for linting, security audits (Gitleaks), automated tests, and Docker releases
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
<%- include('../partials/head', { pageType: 'post' }) %>
|
||||
|
||||
<div class="post-layout-container">
|
||||
<article class="post-article-main">
|
||||
<header class="post-header-editorial">
|
||||
<h1 class="post-title-editorial"><%= post.title %></h1>
|
||||
|
||||
<div class="post-meta-editorial">
|
||||
<time datetime="<%= post.date.toISOString() %>"><%= post.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) %></time>
|
||||
<span class="meta-sep">—</span>
|
||||
<span>by <%= post.author %></span>
|
||||
<% if (post.category) { %>
|
||||
<span>in <a href="/blogs?category=<%= encodeURIComponent(post.category) %>" class="meta-category-link"><%= post.category %></a></span>
|
||||
<% } %>
|
||||
|
||||
<!-- Inline Bilingual Language Switcher with stable line height -->
|
||||
<% if (post.availableLanguages && post.availableLanguages.length > 1) { %>
|
||||
<span class="meta-sep">•</span>
|
||||
<span class="meta-lang-switcher">
|
||||
<span style="opacity: 0.7;">🌐</span>
|
||||
<% post.availableLanguages.forEach(l => {
|
||||
const isCurrent = (currentLang === l);
|
||||
const targetUrl = (l === post.defaultLang || (l === 'en' && post.isDefault))
|
||||
? `/${post.slug}`
|
||||
: `/${post.slug}/${l}`;
|
||||
const displayLabel = l === 'zh' ? '中文' : (l === 'en' ? 'English' : l.toUpperCase());
|
||||
%>
|
||||
<a href="<%= targetUrl %>" class="lang-text-link <%= isCurrent ? 'lang-active' : '' %>">
|
||||
<%= displayLabel %>
|
||||
</a>
|
||||
<% }) %>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<% if (post.cover) { %>
|
||||
<img src="<%= post.cover %>" alt="<%= post.title %>" class="post-featured-image">
|
||||
<% } %>
|
||||
|
||||
<!-- Rendered Markdown Body -->
|
||||
<div class="post-body-content">
|
||||
<%- post.contentHtml %>
|
||||
</div>
|
||||
|
||||
<!-- Unified Tag Links -->
|
||||
<% if (post.tags && post.tags.length > 0) { %>
|
||||
<div class="post-tags-container">
|
||||
<span class="post-tags-label">Tags:</span>
|
||||
<% post.tags.forEach(tag => { %>
|
||||
<a href="/blogs?tag=<%= encodeURIComponent(tag.toLowerCase()) %>" class="tag-link">
|
||||
#<%= tag %>
|
||||
</a>
|
||||
<% }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Plugin Hook: injectPostEnd -->
|
||||
<%- pluginHook('injectPostEnd', { post, pageType: 'post' }) %>
|
||||
|
||||
<!-- Clean Previous / Next Post Navigation -->
|
||||
<% if (prevPost || nextPost) { %>
|
||||
<nav class="post-nav-editorial">
|
||||
<div class="nav-prev">
|
||||
<% if (prevPost) { %>
|
||||
<a href="/<%= prevPost.slug %>" class="editorial-nav-link">
|
||||
← Previous: <%= prevPost.title %>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="nav-next">
|
||||
<% if (nextPost) { %>
|
||||
<a href="/<%= nextPost.slug %>" class="editorial-nav-link">
|
||||
Next: <%= nextPost.title %> →
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</nav>
|
||||
<% } %>
|
||||
|
||||
<!-- Modernized Comments Section -->
|
||||
<%- include('../partials/comments', { post, comments, commentStatus }) %>
|
||||
</article>
|
||||
|
||||
<!-- Ambient Floating Side Rail TOC (Positioned in left margin, Dropbox Paper style) -->
|
||||
<% if (post.toc && post.toc.length > 0) { %>
|
||||
<aside class="desktop-side-toc" id="side-toc">
|
||||
<div class="side-toc-sticky">
|
||||
<div class="side-toc-header">
|
||||
<span class="side-toc-title">Contents</span>
|
||||
<button type="button" class="side-toc-minimize-btn" id="side-toc-minimize" title="Minimize Table of Contents" aria-label="Minimize Table of Contents">—</button>
|
||||
</div>
|
||||
<nav class="side-toc-nav" id="side-toc-nav">
|
||||
<ul class="side-toc-list">
|
||||
<% post.toc.forEach(h => { %>
|
||||
<li class="side-toc-item level-<%= h.level %>">
|
||||
<a href="#<%= h.id %>" class="side-toc-link" data-id="<%= h.id %>">
|
||||
<span class="toc-rail-indicator"></span>
|
||||
<span class="toc-link-text"><%= h.title %></span>
|
||||
</a>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Floating Dock Badge (shown when side TOC is minimized, or on compact/mobile screens) -->
|
||||
<button type="button" class="floating-toc-badge" id="floating-toc-badge" aria-label="Open Table of Contents" title="Table of Contents">
|
||||
<span class="badge-icon">📑</span>
|
||||
<span class="badge-label">Contents</span>
|
||||
<span class="badge-count"><%= post.toc.length %></span>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Slide-Up Drawer Sheet & Dark Backdrop -->
|
||||
<div class="mobile-toc-overlay" id="mobile-toc-overlay"></div>
|
||||
<div class="mobile-toc-sheet" id="mobile-toc-sheet">
|
||||
<div class="mobile-sheet-header">
|
||||
<div class="mobile-sheet-title">
|
||||
<span>📑 Table of Contents</span>
|
||||
<span class="pill" style="font-size: 0.75rem; background: var(--color-surface); border: 1px solid var(--color-border);"><%= post.toc.length %> sections</span>
|
||||
</div>
|
||||
<button type="button" class="mobile-sheet-close" id="mobile-sheet-close" aria-label="Close Table of Contents">×</button>
|
||||
</div>
|
||||
<nav class="mobile-sheet-body">
|
||||
<ol class="mobile-sheet-list">
|
||||
<% post.toc.forEach(h => { %>
|
||||
<li class="mobile-sheet-item level-<%= h.level %>">
|
||||
<a href="#<%= h.id %>" class="mobile-sheet-link"><%= h.title %></a>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<%- include('../partials/foot', { pageType: 'post' }) %>
|
||||
Reference in New Issue
Block a user