feat(blog): recreate article footer navigation and related posts
- add reusable live-site-faithful blog footer component - extract previous, next, and related post data from article content - remove duplicated footer fragments from multilingual articles - document the repeatable footer extraction workflow
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fg from 'fast-glob';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const contentRoot = path.join(root, 'src/content/blog');
|
||||
const output = path.join(root, 'src/data/blog-footers.json');
|
||||
const footerHeading = /^### Similar Blog Posts\s*$/m;
|
||||
const navPattern = /^ ?(Previous Post|Next Post) ?\n\n###### \[([^\]]+)\]\(([^)]+)\)\s*$/gm;
|
||||
const relatedPattern = /!\[([^\]]*)\]\(([^)]+)\)\s*\n\n#### \[([^\]]+)\]\(([^)]+)\)/g;
|
||||
|
||||
const routeFor = (lang, href) => {
|
||||
if (/^(?:https?:|mailto:|tel:|#|\/)/.test(href)) return href;
|
||||
const slug = href.replace(/^\.\.\//, '').replace(/^library\//, '');
|
||||
return `${lang === 'en' ? '' : `/${lang}`}/library/${slug}`;
|
||||
};
|
||||
|
||||
const records = {};
|
||||
for (const file of await fg('**/*.{md,mdx}', { cwd: contentRoot, onlyFiles: true })) {
|
||||
const source = await fs.readFile(path.join(contentRoot, file), 'utf8');
|
||||
const heading = footerHeading.exec(source);
|
||||
if (!heading) continue;
|
||||
|
||||
const [lang, filename] = file.split('/');
|
||||
const slug = filename.replace(/\.(?:md|mdx)$/, '');
|
||||
const before = source.slice(0, heading.index);
|
||||
const footer = source.slice(heading.index);
|
||||
const navigation = {};
|
||||
let firstNavIndex = heading.index;
|
||||
|
||||
for (const match of before.matchAll(navPattern)) {
|
||||
firstNavIndex = Math.min(firstNavIndex, match.index);
|
||||
navigation[match[1] === 'Previous Post' ? 'previous' : 'next'] = {
|
||||
title: match[2],
|
||||
href: routeFor(lang, match[3])
|
||||
};
|
||||
}
|
||||
|
||||
const related = [...footer.matchAll(relatedPattern)].map((match) => ({
|
||||
image: match[2],
|
||||
alt: match[1],
|
||||
title: match[3],
|
||||
href: routeFor(lang, match[4])
|
||||
}));
|
||||
|
||||
records[`${lang}/${slug}`] = { ...navigation, related };
|
||||
await fs.writeFile(path.join(contentRoot, file), `${source.slice(0, firstNavIndex).trimEnd()}\n`);
|
||||
}
|
||||
|
||||
const count = Object.keys(records).length;
|
||||
if (count === 0) {
|
||||
throw new Error('No blog footer fragments found; existing footer data was left unchanged.');
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(output), { recursive: true });
|
||||
await fs.writeFile(output, `${JSON.stringify(records, null, 2)}\n`);
|
||||
console.log(`Extracted and removed footer fragments from ${count} blog posts.`);
|
||||
Reference in New Issue
Block a user