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,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;
|
||||
Reference in New Issue
Block a user