Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
221 lines
8.6 KiB
JavaScript
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, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
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`);
|