Files
bentopdf/scripts/enhance-seo-pages.mjs
T

168 lines
4.9 KiB
JavaScript

import fs from 'fs';
import path from 'path';
import { JSDOM } from 'jsdom';
import { fileURLToPath } from 'url';
import { serializeJsonLd } from './json-ld.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DIST_DIR = path.resolve(__dirname, '../dist');
const SITE_URL = (process.env.SITE_URL || 'https://www.bentopdf.com').replace(
/\/+$/,
''
);
const FALLBACK_SOCIAL_IMAGE = '/images/og-tools.png';
function listTopLevelHtml() {
return fs
.readdirSync(DIST_DIR)
.filter((f) => f.endsWith('.html'))
.map((f) => path.join(DIST_DIR, f));
}
function imageExists(url) {
if (!url) return true;
let pathname;
try {
pathname = url.startsWith('http') ? new URL(url).pathname : url;
} catch {
return true;
}
if (!pathname.startsWith('/images/')) return true;
return fs.existsSync(path.join(DIST_DIR, pathname.replace(/^\//, '')));
}
function fixSocialImages(document) {
const selectors = ['meta[property="og:image"]', 'meta[name="twitter:image"]'];
for (const selector of selectors) {
for (const meta of document.querySelectorAll(selector)) {
const value = meta.getAttribute('content');
if (!imageExists(value)) {
meta.setAttribute('content', `${SITE_URL}${FALLBACK_SOCIAL_IMAGE}`);
}
}
}
}
function fixApplicationCategory(document) {
for (const script of document.querySelectorAll(
'script[type="application/ld+json"]'
)) {
let data;
try {
data = JSON.parse(script.textContent || '');
} catch {
continue;
}
if (
data &&
(data['@type'] === 'SoftwareApplication' ||
data['@type'] === 'WebApplication') &&
data.applicationCategory !== 'UtilitiesApplication'
) {
data.applicationCategory = 'UtilitiesApplication';
script.textContent = serializeJsonLd(data);
}
}
}
function hasLdType(document, typeName) {
return [
...document.querySelectorAll('script[type="application/ld+json"]'),
].some((s) => (s.textContent || '').includes(`"${typeName}"`));
}
function injectFaqSchema(document) {
if (hasLdType(document, 'FAQPage')) return false;
const items = [];
for (const details of document.querySelectorAll('details.faq-d')) {
const summary = details.querySelector('summary');
const answer = details.querySelector('p');
if (!summary || !answer) continue;
const question = summary.textContent.replace(/\s+/g, ' ').trim();
const answerText = answer.textContent.replace(/\s+/g, ' ').trim();
if (!question || !answerText) continue;
items.push({
'@type': 'Question',
name: question,
acceptedAnswer: { '@type': 'Answer', text: answerText },
});
}
if (items.length === 0) return false;
const script = document.createElement('script');
script.setAttribute('type', 'application/ld+json');
script.textContent = serializeJsonLd({
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: items,
});
document.body.appendChild(script);
return true;
}
function injectWebsiteSchema(document) {
if (hasLdType(document, 'WebSite')) return;
const script = document.createElement('script');
script.setAttribute('type', 'application/ld+json');
script.textContent = serializeJsonLd({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'BentoPDF',
alternateName: ['Bento PDF', 'BentoPDF.com'],
url: `${SITE_URL}/`,
});
document.body.appendChild(script);
}
function rewriteInternalLinks(document, knownSlugs) {
for (const link of document.querySelectorAll('a[href]')) {
const href = link.getAttribute('href');
if (!href || /^(https?:|\/\/|#|mailto:|tel:|data:)/.test(href)) continue;
const match = href.match(
/^(?:\.\/|\/)?([a-z0-9][a-z0-9-]{0,64})\.html(#[^#]{0,128})?$/
);
if (!match) continue;
if (!knownSlugs.has(match[1])) continue;
const target = match[1] === 'index' ? '/' : `/${match[1]}`;
link.setAttribute('href', `${target}${match[2] || ''}`);
}
}
function enhance() {
if (!fs.existsSync(DIST_DIR)) {
console.error('enhance-seo-pages: dist not found, run vite build first');
process.exit(1);
}
const files = listTopLevelHtml();
const knownSlugs = new Set(files.map((f) => path.basename(f, '.html')));
let faqSchemaCount = 0;
for (const file of files) {
const slug = path.basename(file, '.html');
const dom = new JSDOM(fs.readFileSync(file, 'utf-8'));
const document = dom.window.document;
fixSocialImages(document);
fixApplicationCategory(document);
rewriteInternalLinks(document, knownSlugs);
if (slug === 'index') {
injectWebsiteSchema(document);
}
if (slug !== '404') {
if (injectFaqSchema(document)) faqSchemaCount++;
}
fs.writeFileSync(file, dom.serialize());
dom.window.close();
}
console.log(
`SEO enhance: ${files.length} pages processed, ${faqSchemaCount} FAQPage schemas added.`
);
}
enhance();