feat(migration): complete WordPress migration and UX refinements
- Migrate historical posts, localized assets, and 103 comments - Colocate media into posts/<slug>/images/ and remove wp-content - Add native bilingual support for ai-can-change post (English default, /zh variant) - Support clean taxonomy routing (/category/:category and /tag/:tag) - Dynamically populate categories and tags in sitemap.xml - Fix header scroll padding/margin offsets for anchor and reference links - Constrain image and media widths to prevent content overflow - Redesign homepage recent posts to 4 compact editorial cards - Restore valid binary favicon.ico and high-res icon assets - Remove placeholder content and sample test comments
This commit is contained in:
+11
-4
@@ -2,9 +2,11 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const contentService = require('../services/content');
|
||||
|
||||
router.get('/blogs', (req, res) => {
|
||||
function renderBlogListing(req, res, filterType, filterVal) {
|
||||
let posts = contentService.getAllPosts();
|
||||
const { category, tag, page } = req.query;
|
||||
const category = filterType === 'category' ? filterVal : req.query.category;
|
||||
const tag = filterType === 'tag' ? filterVal : req.query.tag;
|
||||
const page = req.query.page;
|
||||
|
||||
let activeFilter = null;
|
||||
|
||||
@@ -29,7 +31,7 @@ router.get('/blogs', (req, res) => {
|
||||
const taxonomy = contentService.getTaxonomy();
|
||||
|
||||
res.render('pages/blogs', {
|
||||
title: activeFilter ? `Blogs - ${activeFilter.type}: ${activeFilter.value}` : 'Blogs',
|
||||
title: activeFilter ? `${activeFilter.value} - ${activeFilter.type === 'category' ? 'Category' : 'Tag'} - ${res.locals.site.title}` : `Blogs - ${res.locals.site.title}`,
|
||||
posts: paginatedPosts,
|
||||
currentPage,
|
||||
totalPages,
|
||||
@@ -37,6 +39,11 @@ router.get('/blogs', (req, res) => {
|
||||
activeFilter,
|
||||
taxonomy
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/blogs', (req, res) => renderBlogListing(req, res));
|
||||
router.get('/category/:category', (req, res) => renderBlogListing(req, res, 'category', req.params.category));
|
||||
router.get('/tag/:tag', (req, res) => renderBlogListing(req, res, 'tag', req.params.tag));
|
||||
router.get('/tags/:tag', (req, res) => renderBlogListing(req, res, 'tag', req.params.tag));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -4,7 +4,7 @@ const contentService = require('../services/content');
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
const homePage = contentService.getPage('/');
|
||||
const recentPosts = contentService.getAllPosts().slice(0, 6);
|
||||
const recentPosts = contentService.getAllPosts().slice(0, 4);
|
||||
|
||||
res.render('pages/home', {
|
||||
title: homePage ? homePage.title : res.locals.site.title,
|
||||
|
||||
+11
-2
@@ -16,7 +16,7 @@ function renderPost(req, res, next, slug, lang) {
|
||||
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}` : ''}`,
|
||||
canonicalUrl: `${res.locals.site.url}/${post.slug}${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(),
|
||||
@@ -36,13 +36,22 @@ function renderPost(req, res, next, slug, lang) {
|
||||
|
||||
// Route for bilingual variant: /<post-slug>/<lang> (e.g. /my-post/zh)
|
||||
router.get('/:slug/:lang([a-zA-Z]{2,5})', (req, res, next) => {
|
||||
const post = contentService.getPost(req.params.slug);
|
||||
if (post && post.defaultLang === req.params.lang) {
|
||||
return res.redirect(301, `/${req.params.slug}`);
|
||||
}
|
||||
renderPost(req, res, next, req.params.slug, req.params.lang);
|
||||
});
|
||||
|
||||
// Legacy redirects for multilingual posts
|
||||
router.get('/ai-can-change-coding-for-kids-en', (req, res) => {
|
||||
res.redirect(301, '/ai-can-change-coding-for-kids');
|
||||
});
|
||||
|
||||
// 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'];
|
||||
const reserved = ['blogs', 'category', 'tag', 'tags', '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();
|
||||
}
|
||||
|
||||
@@ -59,6 +59,28 @@ router.get('/sitemap.xml', (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Taxonomy archive pages (Categories & Tags)
|
||||
const taxonomy = contentService.getTaxonomy();
|
||||
if (taxonomy.categories && taxonomy.categories.length > 0) {
|
||||
for (const cat of taxonomy.categories) {
|
||||
xml += ' <url>\n';
|
||||
xml += ` <loc>${siteUrl}/category/${encodeURIComponent(cat.name)}</loc>\n`;
|
||||
xml += ` <changefreq>weekly</changefreq>\n`;
|
||||
xml += ` <priority>0.6</priority>\n`;
|
||||
xml += ' </url>\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (taxonomy.tags && taxonomy.tags.length > 0) {
|
||||
for (const tag of taxonomy.tags) {
|
||||
xml += ' <url>\n';
|
||||
xml += ` <loc>${siteUrl}/tag/${encodeURIComponent(tag.name)}</loc>\n`;
|
||||
xml += ` <changefreq>weekly</changefreq>\n`;
|
||||
xml += ` <priority>0.5</priority>\n`;
|
||||
xml += ' </url>\n';
|
||||
}
|
||||
}
|
||||
|
||||
xml += '</urlset>';
|
||||
|
||||
res.header('Content-Type', 'application/xml; charset=utf-8');
|
||||
|
||||
@@ -145,6 +145,7 @@ class ContentService {
|
||||
defaultPost.availableLanguages = availableLangs;
|
||||
for (const v of Object.values(variants)) {
|
||||
v.availableLanguages = availableLangs;
|
||||
v.defaultLang = defaultPost.lang;
|
||||
}
|
||||
|
||||
this.postsBySlug.set(slug, {
|
||||
|
||||
@@ -80,6 +80,37 @@ class DatabaseService {
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_email ON comments(author_email);
|
||||
`);
|
||||
|
||||
// Auto-seed comments if comments table is newly created and comments.json exists
|
||||
try {
|
||||
const count = this.db.prepare('SELECT COUNT(*) as c FROM comments').get().c;
|
||||
const seedFile = path.join(dataDir, 'comments.json');
|
||||
if (count === 0 && fs.existsSync(seedFile)) {
|
||||
const seedData = JSON.parse(fs.readFileSync(seedFile, 'utf8'));
|
||||
const insertStmt = this.db.prepare(`
|
||||
INSERT INTO comments (id, post_slug, author_name, author_email, author_website, body, created_at, approved, ip)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
this.db.exec('BEGIN');
|
||||
for (const c of seedData) {
|
||||
insertStmt.run(
|
||||
c.id,
|
||||
c.post_slug,
|
||||
c.author_name,
|
||||
c.author_email,
|
||||
c.author_website,
|
||||
c.body,
|
||||
c.created_at,
|
||||
c.approved,
|
||||
c.ip || ''
|
||||
);
|
||||
}
|
||||
this.db.exec('COMMIT');
|
||||
console.log(`[DB] Seeded ${seedData.length} comments from ${seedFile}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[DB] Warning: Failed to auto-seed comments:', err.message);
|
||||
}
|
||||
|
||||
console.log(`[DB] Unified SQLite database initialized at ${this.dbPath}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user