- 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
107 lines
2.8 KiB
JavaScript
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;
|