Files
teknav-bak/app/scripts/generate-author-pages.mjs
arsam c4a24ed219 Import source: app/scripts/generate-author-pages.mjs
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:23:22 +03:30

221 lines
8.6 KiB
JavaScript

import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { TeknavData } from '../teknav-data.js';
import { renderStaticFallback, replaceStaticFallback } from './static-fallback.mjs';
const SITE_URL = (process.env.TEKNAV_SITE_URL || process.env.VITE_SITE_URL || 'https://www.teknav.ir').replace(/\/$/, '');
const template = readFileSync(resolve('dist/index.html'), 'utf8');
function esc(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function clean(value) {
return String(value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
}
function truncate(value, max = 155) {
const text = clean(value);
return text.length > max ? `${text.slice(0, max - 1).trim()}…` : text;
}
function imageUrl(path) {
if (!path) return `${SITE_URL}/favicon.png`;
if (path.startsWith('http')) return path;
return `${SITE_URL}${path.startsWith('/') ? '' : '/'}${path}`;
}
const authors = TeknavData.authors ?? [];
let count = 0;
for (const author of authors) {
const name = author.name;
const description = truncate(author.bio || `آرشیو مقاله‌های ${name} در تکناو.`, 155);
const avatarUrl = author.avatarUrl;
const keywords = `${name}, نویسنده فناوری, تحلیل فناوری, تکناو`;
const sameAs = author.social ? Object.values(author.social).filter(v => v && v !== '#') : [];
if (author.alumniOf?.wikiUrl) sameAs.push(author.alumniOf.wikiUrl);
if (author.alumniOf?.sameAs) sameAs.push(author.alumniOf.sameAs);
if (author.alumniOf?.wikidata) sameAs.push(author.alumniOf.wikidata);
const interactionStatistic = [];
if (author.articleCount) {
interactionStatistic.push({
'@type': 'InteractionCounter',
interactionType: 'https://schema.org/WriteAction',
userInteractionCount: author.articleCount,
});
}
const followerCount = author.followerCount || 1284;
interactionStatistic.push({
'@type': 'InteractionCounter',
interactionType: 'https://schema.org/FollowAction',
userInteractionCount: followerCount,
});
const person = {
'@type': 'Person',
name,
url: `${SITE_URL}/author/${author.slug}`,
jobTitle: author.specialty,
description: author.bio,
knowsAbout: author.expertise,
sameAs,
...(avatarUrl ? { image: imageUrl(avatarUrl) } : {}),
...(author.username ? { alternateName: `@${author.username}` } : {}),
interactionStatistic,
};
if (author.alumniOf) {
person.alumniOf = {
'@type': 'EducationalOrganization',
name: author.alumniOf.name,
url: author.alumniOf.wikiUrl || author.alumniOf.sameAs,
sameAs: author.alumniOf.wikidata,
};
}
if (author.credential) {
person.hasCredential = [
{
'@type': 'EducationalOccupationalCredential',
name: author.credential.name,
credentialCategory: author.credential.credentialCategory || 'degree',
recognizedBy: author.alumniOf ? {
'@type': 'EducationalOrganization',
name: author.alumniOf.name,
url: author.alumniOf.wikiUrl || author.alumniOf.sameAs,
sameAs: author.alumniOf.wikidata,
} : undefined,
}
];
}
// Base Organization & WebSite
const org = {
'@type': ['Organization', 'NewsMediaOrganization'],
'@id': `${SITE_URL}/#organization`,
name: 'تکناو',
alternateName: ['Teknav', 'تکنّاو'],
url: SITE_URL,
logo: {
'@type': 'ImageObject',
'@id': `${SITE_URL}/#logo`,
url: `${SITE_URL}/favicon.png`,
width: 512,
height: 512,
},
image: { '@id': `${SITE_URL}/#logo` },
};
const website = {
'@type': 'WebSite',
'@id': `${SITE_URL}/#website`,
url: SITE_URL,
name: 'تکناو',
alternateName: 'Teknav',
inLanguage: 'fa-IR',
publisher: { '@id': `${SITE_URL}/#organization` },
};
const generatePage = (routeUrl, isProfile = false) => {
const title = `${name} | ${isProfile ? 'پروفایل تکناو' : 'نویسنده تکناو'}`;
const pageKeywords = isProfile ? `${name}, @${author.username}, پروفایل تکناو, نویسنده فناوری` : keywords;
const authorArticles = (TeknavData.articles ?? [])
.filter(article => (
(article.authorId === author.id || article.authorName === author.name || article.authorUsername === author.username)
&& article.status === 'منتشرشده'
))
.slice(0, 12);
const jsonLd = JSON.stringify({
'@context': 'https://schema.org',
'@graph': [
org,
website,
{
'@type': 'ProfilePage',
name: isProfile ? name : `${name} در تکناو`,
url: routeUrl,
inLanguage: 'fa-IR',
...(avatarUrl ? { primaryImageOfPage: imageUrl(avatarUrl) } : {}),
mainEntity: {
...person,
url: routeUrl,
},
},
{
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: 'تکناو', item: SITE_URL },
{ '@type': 'ListItem', position: 2, name: isProfile ? 'نویسندگان' : 'نویسندگان تکناو', item: `${SITE_URL}/authors` },
{ '@type': 'ListItem', position: 3, name },
],
},
],
});
let html = template;
html = html.replace(/<html lang="[^"]*" dir="[^"]*">/, `<html lang="fa" dir="rtl">`);
html = html.replace(/<title>[^<]*<\/title>/, `<title>${title}</title>`);
html = html.replace(/(<meta name="description" content=")[^"]*(")/, `$1${description}$2`);
html = html.replace(/(<meta name="keywords" content=")[^"]*(")/, `$1${esc(pageKeywords)}$2`);
html = html.replace(/(<meta name="language" content=")[^"]*(")/, `$1fa-IR$2`);
html = html.replace(/(<meta property="og:locale" content=")[^"]*(")/, `$1fa_IR$2`);
html = html.replace(/(<meta property="og:title" content=")[^"]*(")/, `$1${title}$2`);
html = html.replace(/(<meta property="og:description" content=")[^"]*(")/, `$1${description}$2`);
html = html.replace(/(<meta property="og:url" content=")[^"]*(")/, `$1${esc(routeUrl)}$2`);
html = html.replace(/(<meta property="og:image" content=")[^"]*(")/, `$1${esc(imageUrl(avatarUrl))}$2`);
html = html.replace(/(<meta name="twitter:title" content=")[^"]*(")/, `$1${title}$2`);
html = html.replace(/(<meta name="twitter:description" content=")[^"]*(")/,`$1${description}$2`);
html = html.replace(/(<meta name="twitter:image" content=")[^"]*(")/, `$1${esc(imageUrl(avatarUrl))}$2`);
html = html.replace(/(<link rel="canonical" href=")[^"]*(")/, `$1${esc(routeUrl)}$2`);
html = html.replace(/<link rel="alternate" hreflang="[^"]+" href="[^"]+" \/>/, `<link rel="alternate" hreflang="fa" href="${esc(routeUrl)}" />`);
html = html.replace(/<script type="application\/ld\+json">[\s\S]*?<\/script>/,
`<script type="application/ld+json">${jsonLd}</script>`);
html = replaceStaticFallback(html, renderStaticFallback({
eyebrow: isProfile ? 'پروفایل نویسنده' : 'نویسنده تکناو',
title,
description: author.bio || description,
facts: [
author.specialty ? { label: 'تخصص', value: author.specialty } : null,
author.expertise?.length ? { label: 'حوزه‌ها', value: author.expertise.join('، ') } : null,
author.education ? { label: 'تحصیلات', value: author.education } : null,
].filter(Boolean),
links: [
...authorArticles.map(article => ({
href: article.canonicalPath || `/article/${article.slug}`,
label: article.title,
text: article.summary,
})),
{ href: '/authors', label: 'همه نویسندگان', text: 'آشنایی با تیم تحریریه تکناو' },
],
}));
return html;
};
// 1. Generate /author/${slug}/index.html
const authorHtml = generatePage(`${SITE_URL}/author/${author.slug}`, false);
const authorDir = resolve(`dist/author/${author.slug}`);
mkdirSync(authorDir, { recursive: true });
writeFileSync(`${authorDir}/index.html`, authorHtml, 'utf8');
count++;
// 2. Generate /profile/@${username}/index.html
if (author.username) {
const profileHtml = generatePage(`${SITE_URL}/profile/@${author.username}`, true);
const profileDir = resolve(`dist/profile/@${author.username}`);
mkdirSync(profileDir, { recursive: true });
writeFileSync(`${profileDir}/index.html`, profileHtml, 'utf8');
count++;
}
}
console.log(`[author-pages] generated ${count} static author/profile HTML pages`);