Files
pengzhan.dev/server/middleware/seo.js
T
haopengzhan 8c0b04db0e 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
2026-09-16 23:08:53 -07:00

107 lines
2.8 KiB
JavaScript

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;