import fs from 'fs';
import path from 'path';
import { marked } from 'marked';
import { fileURLToPath } from 'url';
import { serializeJsonLd } from './json-ld.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const POSTS_DIR = path.resolve(__dirname, '../blog/posts');
const BLOG_DIR = path.resolve(__dirname, '../blog');
const SITE_URL = 'https://www.bentopdf.com';
const AUTHOR = {
name: 'Alam',
url: `${SITE_URL}/blog/author-alam`,
image: `${SITE_URL}/images/author-alam.jpg`,
profiles: [
'https://github.com/alam00000',
'https://www.linkedin.com/in/abdullah-alam01/',
'https://x.com/alamdraws',
],
};
const LINK_CLASS = 'text-indigo-300 hover:text-indigo-400';
function escapeHtml(value) {
return value
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function styleBody(html) {
return html
.replace(/
/g, '')
.replace(//g, '')
.replace(/
/g, '
')
.replace(
/
/g,
''
)
.replace(
//g,
''
)
.replace(
/#]/g, '')
.replace(/^\s*\d+\.\s+/gm, '')
.replace(/^\s*[-+]\s+/gm, '')
.replace(/\s+/g, ' ')
.trim();
}
function questionSections(body) {
const sections = [];
const parts = body.split(/^##\s+/m).slice(1);
for (const part of parts) {
const newline = part.indexOf('\n');
const heading = (newline === -1 ? part : part.slice(0, newline)).trim();
if (!heading.endsWith('?')) continue;
const answer = plainText(newline === -1 ? '' : part.slice(newline + 1));
if (!answer) continue;
sections.push({ q: heading, a: answer.slice(0, 600) });
}
return sections;
}
function faqSchema(entries) {
if (entries.length === 0) return '';
return `
`;
}
function renderFaqs(faqs) {
if (!faqs || faqs.length === 0) return '';
const items = faqs
.map(
(f) => `
${escapeHtml(f.q)}
${renderInline(f.a)}
`
)
.join('\n');
return `
FAQ
${items}
`;
}
function renderCta(cta) {
if (!cta) return '';
return `
`;
}
function renderPost({ slug, meta, body }) {
const url = `${SITE_URL}/blog/${slug}`;
const updated = meta.updated || meta.date;
const bodyHtml = styleBody(marked.parse(body));
const faqEntries =
meta.faqs && meta.faqs.length > 0 ? meta.faqs : questionSections(body);
return `
${escapeHtml(meta.title)}
{{> navbar }}
${escapeHtml(meta.h1)}
${bodyHtml}
${renderCta(meta.cta)}
${renderFaqs(meta.faqs)}
{{> footer }}
${faqSchema(faqEntries)}
`;
}
function renderIndex(posts) {
const cards = posts
.map(
({ slug, meta }) => `
${escapeHtml(meta.h1)}
${escapeHtml(meta.card)}
${formatDate(meta.date)}
`
)
.join('\n\n');
return `
The BentoPDF Blog | Privacy-First PDF Guides
{{> navbar }}
The BentoPDF Blog
I'm Alam. I build
BentoPDF and write about PDFs, how they work, and the weird things I
run into along the way.
{{> footer }}
`;
}
function generate() {
if (!fs.existsSync(POSTS_DIR)) {
console.log('generate-blog: no blog/posts directory, skipping');
return;
}
const files = fs
.readdirSync(POSTS_DIR)
.filter((f) => f.endsWith('.md'))
.sort();
const posts = files.map(parsePost);
posts.sort((a, b) => (a.meta.date < b.meta.date ? 1 : -1));
const keep = new Set(['index.html', 'author-alam.html']);
for (const post of posts) {
fs.writeFileSync(
path.join(BLOG_DIR, `${post.slug}.html`),
renderPost(post)
);
keep.add(`${post.slug}.html`);
}
fs.writeFileSync(path.join(BLOG_DIR, 'index.html'), renderIndex(posts));
for (const entry of fs.readdirSync(BLOG_DIR)) {
if (entry.endsWith('.html') && !keep.has(entry)) {
fs.rmSync(path.join(BLOG_DIR, entry));
console.log(`generate-blog: removed stale ${entry}`);
}
}
console.log(
`generate-blog: ${posts.length} posts + index generated from blog/posts/`
);
}
generate();