Import source: app/src/lib/seo

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:24:44 +03:30
parent 92eddb8af0
commit 9a6da5fa31
+974
View File
@@ -0,0 +1,974 @@
import { useEffect, useMemo, useState } from 'react';
import { TeknavData } from '../../teknav-data.js';
import { api } from './api.js';
import { contentApi } from './content-api.js';
import { useNav } from '../../teknav-ui.jsx';
const SITE_URL = (import.meta.env.VITE_SITE_URL || 'https://www.teknav.ir').replace(/\/$/, '');
const SITE_NAME = 'تکناو';
const DEFAULT_TITLE = 'تکناو | تحلیل فناوری، هوش مصنوعی و امنیت سایبری';
const DEFAULT_DESCRIPTION = 'تکناو رسانه فارسی تحلیل فناوری، هوش مصنوعی، علم داده، امنیت سایبری، نرم‌افزار، سخت‌افزار و آینده تکنولوژی است.';
const DEFAULT_KEYWORDS = 'تکناو, فناوری, هوش مصنوعی, علم داده, امنیت سایبری, نرم افزار, سخت افزار, استارتاپ, آینده فناوری, تحلیل تکنولوژی';
const DEFAULT_IMAGE = `${SITE_URL}/images/og/default.jpg`;
function cleanText(value, fallback = '') {
return String(value ?? fallback)
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function truncate(value, max = 155) {
const text = cleanText(value);
return text.length > max ? `${text.slice(0, max - 1).trim()}…` : text;
}
function absoluteUrl(path = '/') {
if (/^https?:\/\//i.test(path)) return path;
const normalized = path.startsWith('/') ? path : `/${path}`;
return `${SITE_URL}${normalized}`;
}
function imageUrl(value) {
return value ? absoluteUrl(value) : DEFAULT_IMAGE;
}
function uniqueList(values, limit = 18) {
const seen = new Set();
return values
.map((value) => cleanText(value))
.filter(Boolean)
.filter((value) => {
const key = value.toLocaleLowerCase('fa-IR');
if (seen.has(key)) return false;
seen.add(key);
return true;
})
.slice(0, limit);
}
function articleKeywords(article, category, author) {
return uniqueList([
...(article.keywords || []),
...(article.tags || []),
article.title,
article.subtitle,
article.categoryName || category?.name,
article.type,
author?.specialty,
'تحلیل فارسی فناوری',
'تکناو',
]);
}
const ENTITY_WIKIDATA_MAP = {
'هوش مصنوعی': 'https://www.wikidata.org/wiki/Q11660',
'ai': 'https://www.wikidata.org/wiki/Q11660',
'artificial intelligence': 'https://www.wikidata.org/wiki/Q11660',
'rag': 'https://www.wikidata.org/wiki/Q108745802',
'retrieval-augmented generation': 'https://www.wikidata.org/wiki/Q108745802',
'امنیت سایبری': 'https://www.wikidata.org/wiki/Q3510502',
'cybersecurity': 'https://www.wikidata.org/wiki/Q3510502',
'علم داده': 'https://www.wikidata.org/wiki/Q19847',
'data science': 'https://www.wikidata.org/wiki/Q19847',
'رایانش کوانتومی': 'https://www.wikidata.org/wiki/Q11190',
'quantum computing': 'https://www.wikidata.org/wiki/Q11190',
'نرم‌افزار': 'https://www.wikidata.org/wiki/Q7397',
'software': 'https://www.wikidata.org/wiki/Q7397',
'سخت‌افزار': 'https://www.wikidata.org/wiki/Q11641',
'hardware': 'https://www.wikidata.org/wiki/Q11641',
'استارتاپ': 'https://www.wikidata.org/wiki/Q300627',
'startup': 'https://www.wikidata.org/wiki/Q300627',
'پایگاه داده': 'https://www.wikidata.org/wiki/Q8513',
'database': 'https://www.wikidata.org/wiki/Q8513',
'امنیت': 'https://www.wikidata.org/wiki/Q2526255',
'security': 'https://www.wikidata.org/wiki/Q2526255',
'شبکه': 'https://www.wikidata.org/wiki/Q1301371',
'network': 'https://www.wikidata.org/wiki/Q1301371',
'اینترنت': 'https://www.wikidata.org/wiki/Q75',
'internet': 'https://www.wikidata.org/wiki/Q75',
'وب': 'https://www.wikidata.org/wiki/Q466',
'web': 'https://www.wikidata.org/wiki/Q466',
'فناوری': 'https://www.wikidata.org/wiki/Q11016',
'technology': 'https://www.wikidata.org/wiki/Q11016',
};
const GLOSSARY_SAMEAS_MAP = {
ai: [
'https://www.wikidata.org/wiki/Q11660',
'https://fa.wikipedia.org/wiki/%D9%87%D9%88%D8%B4_%D9%85%D8%B5%D9%86%D9%88%D8%B5%DB%8C'
],
llm: [
'https://www.wikidata.org/wiki/Q115305900',
'https://fa.wikipedia.org/wiki/%D9%85%D8%AF%D9%84_%D8%B2%D8%A8%D8%A7%D9%86%DB%8C_%D8%A8%D8%B2%D8%B1%DA%AF'
],
rag: [
'https://www.wikidata.org/wiki/Q124378129',
'https://en.wikipedia.org/wiki/Retrieval-augmented_generation'
],
'prompt-injection': [
'https://www.wikidata.org/wiki/Q117409023',
'https://en.wikipedia.org/wiki/Prompt_injection'
],
'vector-database': [
'https://www.wikidata.org/wiki/Q119790697',
'https://en.wikipedia.org/wiki/Vector_database'
],
'agentic-ai': [
'https://www.wikidata.org/wiki/Q127415174'
]
};
function renderAuthorPerson(author, defaultName, defaultJob, defaultBio, defaultExpertise, defaultUrl, defaultImage) {
if (!author) {
const avatar = defaultImage ? imageUrl(defaultImage) : undefined;
return {
'@type': 'Person',
name: defaultName,
jobTitle: defaultJob,
description: defaultBio,
knowsAbout: defaultExpertise,
url: defaultUrl,
...(avatar ? { image: avatar } : {}),
};
}
const name = author.name || defaultName;
const url = defaultUrl || (author.slug ? absoluteUrl(`/author/${author.slug}`) : undefined);
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 avatarUrl = author.avatarUrl || defaultImage;
const image = avatarUrl ? imageUrl(avatarUrl) : undefined;
const alternateName = author.username ? `@${author.username}` : undefined;
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,
jobTitle: author.specialty || defaultJob,
description: author.bio || defaultBio,
knowsAbout: author.expertise || defaultExpertise,
sameAs,
...(image ? { image } : {}),
...(alternateName ? { alternateName } : {}),
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 || author.credential.category || 'degree',
};
}
return person;
}
function articleEntities(article, category) {
return uniqueList([
article.categoryName || category?.name,
...(article.tags || []),
...(article.keywords || []),
], 24).map((name) => {
const key = name.toLocaleLowerCase('fa-IR').trim();
const sameAs = ENTITY_WIKIDATA_MAP[key];
return {
'@type': 'Thing',
name,
...(sameAs ? { sameAs } : {}),
};
});
}
function articleSections(content) {
return [...String(content || '').matchAll(/<h2[^>]*>(.*?)<\/h2>/g)]
.map((match) => cleanText(match[1]))
.filter(Boolean)
.slice(0, 12);
}
function articleCitationUrls(article) {
return uniqueList(article?.citationUrls || [], 20);
}
function articleFaqJsonLd(article, path) {
if (!article?.faqs?.length) return null;
return {
'@type': 'FAQPage',
'@id': `${absoluteUrl(path)}#faq`,
mainEntity: article.faqs.map(([question, answer]) => ({
'@type': 'Question',
name: cleanText(question),
acceptedAnswer: {
'@type': 'Answer',
text: cleanText(answer),
},
})),
};
}
function articleBenchmarkJsonLd(article, author, path) {
if (!article?.identifier && !article?.version && !article?.researchQuestions?.length) return null;
return {
'@type': 'CreativeWork',
'@id': `${absoluteUrl(path)}#benchmark`,
name: `${cleanText(article.title)} benchmark protocol`,
alternateName: cleanText(article.slug),
identifier: article.identifier,
version: article.version,
url: absoluteUrl(path),
inLanguage: article.language || article.inLanguage || 'fa-IR',
creator: {
'@type': 'Person',
name: article.authorName || author?.name,
url: author ? absoluteUrl(`/author/${author.slug}`) : undefined,
},
about: (article.researchQuestions || []).map((question) => ({
'@type': 'Question',
name: cleanText(question),
})),
conditionsOfAccess: 'Protocol article; empirical dataset release depends on copyright and licensing clearance.',
};
}
function articleAcademicMeta(article, author, canonical) {
const published = article.publishedAt || article.dateEn;
const modified = article.dateModified || article.updatedAt || published;
return {
citation_title: cleanText(article.title),
citation_author: cleanText(article.authorName || author?.name || ''),
citation_publication_date: published,
citation_online_date: published,
citation_lastdate: modified,
citation_journal_title: 'Teknav',
citation_language: article.language || article.inLanguage || 'fa-IR',
citation_public_url: canonical,
citation_keywords: articleKeywords(article, findCategory(article), author).join('; '),
'DC.title': cleanText(article.title),
'DC.creator': cleanText(article.authorName || author?.name || ''),
'DC.date': published,
'DC.language': article.language || article.inLanguage || 'fa-IR',
'DC.identifier': article.identifier || canonical,
};
}
function setMeta(attr, key, content) {
if (!content) return;
let el = document.head.querySelector(`meta[${attr}="${key}"]`);
if (!el) {
el = document.createElement('meta');
el.setAttribute(attr, key);
document.head.appendChild(el);
}
el.setAttribute('content', content);
}
function setNamedMetaList(name, values) {
document.head.querySelectorAll(`meta[name="${name}"][data-teknav-seo-list="true"]`).forEach((el) => el.remove());
values.filter(Boolean).forEach((value) => {
const el = document.createElement('meta');
el.setAttribute('name', name);
el.dataset.teknavSeoList = 'true';
el.setAttribute('content', value);
document.head.appendChild(el);
});
}
function setLink(rel, href, extra = {}) {
let el = document.head.querySelector(`link[rel="${rel}"]${extra.hreflang ? `[hreflang="${extra.hreflang}"]` : ''}`);
if (!el) {
el = document.createElement('link');
el.setAttribute('rel', rel);
if (extra.hreflang) el.setAttribute('hreflang', extra.hreflang);
document.head.appendChild(el);
}
el.setAttribute('href', href);
}
function setJsonLd(items) {
document.head.querySelectorAll('script[data-teknav-seo="jsonld"]').forEach((el) => el.remove());
const script = document.createElement('script');
script.type = 'application/ld+json';
script.dataset.teknavSeo = 'jsonld';
script.textContent = JSON.stringify({
'@context': 'https://schema.org',
'@graph': items.filter(Boolean),
});
document.head.appendChild(script);
}
function baseGraph() {
return [
{
'@type': ['Organization', 'NewsMediaOrganization'],
'@id': `${SITE_URL}/#organization`,
name: SITE_NAME,
alternateName: ['Teknav', 'تکنّاو'],
url: SITE_URL,
description: DEFAULT_DESCRIPTION,
logo: {
'@type': 'ImageObject',
'@id': `${SITE_URL}/#logo`,
url: `${SITE_URL}/favicon.png`,
width: { '@type': 'QuantitativeValue', value: 512 },
height: { '@type': 'QuantitativeValue', value: 512 },
caption: 'تکناو - رسانه فارسی فناوری',
},
image: { '@id': `${SITE_URL}/#logo` },
publishingPrinciples: `${SITE_URL}/about`,
masthead: `${SITE_URL}/authors`,
areaServed: { '@type': 'Country', name: 'Iran' },
audience: { '@type': 'Audience', audienceType: 'فارسی‌زبانان علاقه‌مند به فناوری' },
sameAs: [
'https://twitter.com/teknavir',
'https://x.com/teknavir',
'https://linkedin.com/company/teknav',
],
contactPoint: {
'@type': 'ContactPoint',
email: 'info@teknav.ir',
contactType: 'customer service',
availableLanguage: { '@type': 'Language', name: 'Persian' },
},
},
{
'@type': 'WebSite',
'@id': `${SITE_URL}/#website`,
url: SITE_URL,
name: SITE_NAME,
alternateName: 'Teknav',
description: DEFAULT_DESCRIPTION,
inLanguage: 'fa-IR',
publisher: { '@id': `${SITE_URL}/#organization` },
potentialAction: {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${SITE_URL}/search?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
},
},
];
}
function breadcrumb(items) {
return {
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.path ? absoluteUrl(item.path) : undefined,
})),
};
}
function itemList(items, path) {
return {
'@type': 'ItemList',
url: absoluteUrl(path),
itemListElement: items.slice(0, 15).map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
url: absoluteUrl(item.canonicalPath || `/article/${item.slug}`),
name: item.title,
})),
};
}
function findCategory(article) {
return TeknavData.categories.find((c) => (
c.id === article?.category ||
c.slug === article?.category ||
c.slug === article?.categorySlug ||
c.name === article?.categoryName
));
}
function findAuthor(article) {
return TeknavData.authors.find((a) => (
a.id === article?.authorId ||
a.slug === article?.authorSlug ||
a.name === article?.authorName
));
}
function articleSeo(article) {
const category = findCategory(article);
const author = findAuthor(article);
const path = article.canonicalPath || `/article/${article.slug}`;
const title = article.seoTitle || article.ogTitle || `${article.title} | تکناو`;
const description = truncate(article.metaDescription || article.ogDescription || article.summary || article.subtitle || article.content, 160);
const datePublished = article.publishedAt || article.dateEn;
const dateModified = article.dateModified || article.updatedAt || article.publishedAt || article.dateEn;
const image = imageUrl(article.ogImage || article.imageUrl || article.coverUrl);
const keywords = articleKeywords(article, category, author);
const sections = articleSections(article.content);
const language = article.language || article.inLanguage || 'fa-IR';
const canonical = absoluteUrl(path);
const citations = articleCitationUrls(article);
const articleTypes = article.identifier || citations.length
? ['NewsArticle', 'TechArticle', 'ScholarlyArticle']
: ['NewsArticle', 'TechArticle'];
const about = [
category ? {
'@type': 'Thing',
name: category.name,
url: absoluteUrl(`/topics/${category.slug}`),
sameAs: ENTITY_WIKIDATA_MAP[category.slug.toLocaleLowerCase('fa-IR')] || ENTITY_WIKIDATA_MAP[category.name.toLocaleLowerCase('fa-IR')],
} : null,
...articleEntities(article, category).slice(0, 5),
].filter(Boolean);
const mentions = articleEntities(article, category);
return {
title,
description,
keywords: keywords.join(', '),
language,
canonical,
type: 'article',
image,
academicMeta: articleAcademicMeta(article, author, canonical),
citations,
jsonLd: [
...baseGraph(),
{
'@type': articleTypes,
'@id': `${canonical}#article`,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': canonical,
},
headline: cleanText(article.title).slice(0, 110),
alternativeHeadline: cleanText(article.subtitle || article.summary).slice(0, 110) || undefined,
description,
abstract: cleanText(article.summary || article.subtitle || description),
articleBody: truncate(article.content, 5000),
image: {
'@type': 'ImageObject',
url: image,
width: 1200,
height: 630
},
datePublished,
dateModified,
inLanguage: language,
articleSection: article.categoryName || category?.name,
keywords,
wordCount: cleanText(article.content).split(/\s+/).filter(Boolean).length || undefined,
timeRequired: article.readTime ? `PT${article.readTime}M` : undefined,
genre: article.type,
teaches: sections,
identifier: article.identifier,
version: article.version,
citation: citations,
author: renderAuthorPerson(author, article.authorName, author?.specialty, author?.bio, author?.expertise),
publisher: { '@id': `${SITE_URL}/#organization` },
about,
mentions,
isPartOf: [
{ '@id': `${SITE_URL}/#website` },
category ? { '@type': 'CollectionPage', '@id': absoluteUrl(`/topics/${category.slug}`) } : null,
].filter(Boolean),
speakable: {
'@type': 'SpeakableSpecification',
xpath: ['/html/head/title', '/html/head/meta[@name="description"]/@content']
},
isAccessibleForFree: true,
educationalLevel: 'Intermediate',
...(article.factCheckedAt ? {
reviewedBy: { '@id': `${SITE_URL}/#organization` },
lastReviewed: article.factCheckedAt
} : {})
},
articleBenchmarkJsonLd(article, author, path),
articleFaqJsonLd(article, path),
breadcrumb([
{ name: SITE_NAME, path: '/' },
{ name: 'مقاله‌ها', path: '/articles' },
category ? { name: category.name, path: `/category/${category.slug}` } : null,
{ name: article.title },
].filter(Boolean)),
],
};
}
function pageSeo(page, data) {
if (page.startsWith('/article/') && data.article) return articleSeo(data.article);
if (page.startsWith('/category/')) {
const slug = page.replace('/category/', '');
const category = data.category || TeknavData.categories.find((c) => c.slug === slug || c.id === slug);
const name = category?.name || 'دسته‌بندی فناوری';
const description = truncate(category?.description || `مقاله‌ها و تحلیل‌های فارسی تکناو درباره ${name}.`, 155);
return {
title: `${name} | مقاله‌ها و تحلیل‌های تکناو`,
description,
keywords: `${name}, مقاله ${name}, تحلیل ${name}, فناوری, تکناو`,
canonical: absoluteUrl(`/category/${slug}`),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [
...baseGraph(),
{ '@type': 'CollectionPage', name, description, url: absoluteUrl(`/category/${slug}`), inLanguage: 'fa-IR' },
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: name }]),
],
};
}
if (page.startsWith('/tag/')) {
const tag = decodeURIComponent(page.replace('/tag/', ''));
const articles = TeknavData.articles
.filter((article) => article.tags?.includes(tag) && article.status !== 'پیش‌نویس' && article.status !== 'در انتظار بررسی')
.sort((a, b) => String(b.dateEn || '').localeCompare(String(a.dateEn || '')));
return {
title: `برچسب ${tag} | مقاله‌های تکنـاو`,
description: truncate(`همه مقاله‌های تکنـاو درباره ${tag}، همراه با تحلیل فارسی، داده و مسیرهای مرتبط برای مطالعه بیشتر.`, 155),
keywords: `${tag}, تکنـاو, مقاله فناوری, تحلیل فناوری`,
canonical: absoluteUrl(`/tag/${encodeURIComponent(tag)}`),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [
...baseGraph(),
{
'@type': 'CollectionPage',
name: `برچسب ${tag}`,
url: absoluteUrl(`/tag/${encodeURIComponent(tag)}`),
inLanguage: 'fa-IR',
mainEntity: itemList(articles, `/tag/${encodeURIComponent(tag)}`),
},
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: 'برچسب‌ها', path: '/articles' }, { name: tag }]),
],
};
}
if (page.startsWith('/glossary/')) {
const slug = page.replace('/glossary/', '');
const term = TeknavData.glossary?.find((item) => item.slug === slug);
const path = `/glossary/${slug}`;
const title = term ? `${term.term} چیست؟ | فرهنگ واژگان تکنّاو` : 'فرهنگ واژگان فناوری | تکنّاو';
const description = truncate(term?.summary || 'تعریف دقیق و فارسی واژه‌های فنی فناوری، هوش مصنوعی، داده، امنیت و نرم‌افزار در تکنّاو.', 160);
return {
title,
description,
keywords: [term?.term, term?.english, 'فرهنگ واژگان فناوری', 'تکنّاو'].filter(Boolean).join(', '),
canonical: absoluteUrl(path),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [
...baseGraph(),
{
'@type': 'DefinedTerm',
name: term?.term,
alternateName: term?.english,
description,
inDefinedTermSet: absoluteUrl('/glossary'),
url: absoluteUrl(path),
sameAs: GLOSSARY_SAMEAS_MAP[slug] || [],
},
term?.faqs?.length ? {
'@type': 'FAQPage',
mainEntity: term.faqs.map(([question, answer]) => ({
'@type': 'Question',
name: question,
acceptedAnswer: { '@type': 'Answer', text: answer },
})),
} : null,
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: 'فرهنگ واژگان', path: '/articles' }, { name: term?.term || slug }]),
].filter(Boolean),
};
}
if (page.startsWith('/topics/')) {
const slug = page.replace('/topics/', '');
const topic = TeknavData.topicHubs.find((item) => item.slug === slug);
const category = TeknavData.categories.find((item) => item.slug === topic?.categorySlug);
const articles = TeknavData.articles
.filter((article) => article.category === topic?.categorySlug && article.status !== 'پیش‌نویس' && article.status !== 'در انتظار بررسی')
.sort((a, b) => String(b.dateEn || '').localeCompare(String(a.dateEn || '')));
const name = topic?.title || category?.name || 'موضوع فناوری';
const description = truncate(topic?.description || category?.description || `مقاله‌ها و تحلیل‌های تکناو درباره ${name}.`, 160);
const path = `/topics/${slug}`;
return {
title: topic?.seoTitle || `${name} | خوشه موضوعی تکناو`,
description,
keywords: (topic?.keywords ?? [name, 'تکناو', 'تحلیل فناوری']).join(', '),
canonical: absoluteUrl(path),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [
...baseGraph(),
{
'@type': 'CollectionPage',
name,
description,
url: absoluteUrl(path),
inLanguage: 'fa-IR',
isPartOf: { '@id': `${SITE_URL}/#website` },
mainEntity: itemList(articles, path),
},
topic?.faqs?.length ? {
'@type': 'FAQPage',
mainEntity: topic.faqs.map(([question, answer]) => ({
'@type': 'Question',
name: question,
acceptedAnswer: { '@type': 'Answer', text: answer },
})),
} : null,
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: 'موضوعات', path: '/articles' }, { name }]),
],
};
}
if (page.startsWith('/series/')) {
const slug = page.replace('/series/', '');
const series = data.series;
const path = `/series/${slug}`;
const name = series?.title || 'سری مقاله تکناو';
const description = truncate(series?.description || 'سری مقاله‌های تکناو برای دنبال کردن یک موضوع فناوری به‌ترتیب تحریریه.', 160);
const articles = (series?.articles ?? []).map((item) => item.article).filter(Boolean);
return {
title: `${name} | سری مقاله‌های تکناو`,
description,
keywords: `${name}, سری مقاله, تکناو, تحلیل فناوری`,
canonical: absoluteUrl(path),
type: 'website',
image: imageUrl(series?.coverImage),
jsonLd: [
...baseGraph(),
{
'@type': 'CollectionPage',
name,
description,
url: absoluteUrl(path),
inLanguage: 'fa-IR',
mainEntity: itemList(articles, path),
},
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: 'سری مقاله‌ها', path: '/articles' }, { name }]),
],
};
}
if (page.startsWith('/author/')) {
const slug = page.replace('/author/', '');
const author = data.author || TeknavData.authors.find((a) => a.slug === slug);
const name = author?.name || 'نویسنده تکناو';
const description = truncate(author?.bio || `آرشیو مقاله‌های ${name} در تکناو.`, 155);
const avatarUrl = author?.avatarUrl;
return {
title: `${name} | نویسنده تکناو`,
description,
keywords: `${name}, نویسنده فناوری, تحلیل فناوری, تکناو`,
canonical: absoluteUrl(`/author/${slug}`),
type: 'profile',
image: imageUrl(avatarUrl),
jsonLd: [
...baseGraph(),
{
'@type': 'ProfilePage',
name: `${name} در تکناو`,
url: absoluteUrl(`/author/${slug}`),
inLanguage: 'fa-IR',
...(avatarUrl ? { primaryImageOfPage: imageUrl(avatarUrl) } : {}),
mainEntity: renderAuthorPerson(author, name, author?.specialty, description, author?.expertise, absoluteUrl(`/author/${slug}`)),
},
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: 'نویسندگان', path: '/authors' }, { name }]),
],
};
}
if (page.startsWith('/profile/@')) {
const username = page.replace('/profile/@', '');
const author = TeknavData.authors.find((a) => a.username === username || a.slug === username);
const name = data.profile?.name || author?.name || `@${username}`;
const description = truncate(data.profile?.bio || author?.bio || `پروفایل عمومی ${name} در تکناو.`, 155);
const avatarUrl = data.profile?.avatarUrl || author?.avatarUrl;
const mainEntity = author
? renderAuthorPerson(author, name, author.specialty, description, author.expertise, absoluteUrl(`/profile/@${username}`), data.profile?.avatarUrl)
: {
'@type': 'Person',
name,
alternateName: `@${username}`,
description,
image: avatarUrl ? imageUrl(avatarUrl) : undefined,
};
return {
title: `${name} | پروفایل تکناو`,
description,
keywords: `${name}, ${username}, پروفایل تکناو, نویسنده فناوری`,
canonical: absoluteUrl(`/profile/@${username}`),
type: 'profile',
image: imageUrl(avatarUrl),
jsonLd: [
...baseGraph(),
{
'@type': 'ProfilePage',
name,
description,
url: absoluteUrl(`/profile/@${username}`),
inLanguage: 'fa-IR',
...(avatarUrl ? { primaryImageOfPage: imageUrl(avatarUrl) } : {}),
mainEntity,
},
],
};
}
if (page === '/articles') {
return {
title: 'مقاله‌های فناوری | تکناو',
description: 'آرشیو مقاله‌های فارسی تکناو درباره هوش مصنوعی، علم داده، امنیت سایبری، نرم‌افزار، سخت‌افزار و آینده فناوری.',
keywords: DEFAULT_KEYWORDS,
canonical: absoluteUrl('/articles'),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [...baseGraph(), { '@type': 'CollectionPage', name: 'مقاله‌های فناوری تکناو', url: absoluteUrl('/articles'), inLanguage: 'fa-IR' }],
};
}
if (page === '/authors') {
return {
title: 'نویسندگان تکناو | تحلیلگران فناوری',
description: 'آشنایی با نویسندگان و تحلیلگران تکناو در حوزه هوش مصنوعی، امنیت سایبری، نرم‌افزار، سخت‌افزار و استارتاپ.',
keywords: 'نویسندگان تکناو, تحلیلگر فناوری, نویسنده هوش مصنوعی, نویسنده امنیت سایبری',
canonical: absoluteUrl('/authors'),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [...baseGraph(), { '@type': 'CollectionPage', name: 'نویسندگان تکناو', url: absoluteUrl('/authors'), inLanguage: 'fa-IR' }],
};
}
if (page === '/newsletter') {
return {
title: 'آرشیو خبرنامه تکنـاو | تحلیل هفتگی فناوری',
description: 'آرشیو خبرنامه‌های ارسال‌شده تکنـاو درباره هوش مصنوعی، امنیت سایبری، نرم‌افزار، سخت‌افزار، استارتاپ و داده.',
keywords: 'خبرنامه تکنـاو, آرشیو خبرنامه فناوری, تحلیل هفتگی فناوری, هوش مصنوعی, امنیت سایبری',
canonical: absoluteUrl('/newsletter'),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [
...baseGraph(),
{ '@type': 'CollectionPage', name: 'آرشیو خبرنامه تکنـاو', url: absoluteUrl('/newsletter'), inLanguage: 'fa-IR' },
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: 'خبرنامه' }]),
],
};
}
if (page === '/jobs' || page === '/courses' || page === '/membership') {
const meta = {
'/jobs': ['فرصت‌های شغلی فناوری | تکناو', 'آگهی‌های شغلی منتخب فناوری و امکان ثبت موقعیت شغلی شرکت‌ها در تکناو.', 'شغل فناوری, استخدام برنامه نویس, استخدام هوش مصنوعی, تکناو جابز'],
'/courses': ['دوره‌های تخصصی فناوری | تکناو', 'دوره‌های فارسی تکناو برای هوش مصنوعی، امنیت، نرم‌افزار و مهارت‌های فنی محصول‌محور.', 'دوره فناوری, آموزش هوش مصنوعی, آموزش امنیت, دوره فارسی برنامه نویسی'],
'/membership': ['عضویت پریمیوم تکناو | محتوای اختصاصی فناوری', 'عضویت پریمیوم تکناو برای تجربه بدون تبلیغ، محتوای اختصاصی و دسترسی بهتر به دوره‌ها و گزارش‌های تخصصی.', 'عضویت تکناو, محتوای پریمیوم فناوری, تحلیل اختصاصی فناوری'],
}[page];
return {
title: meta[0],
description: meta[1],
keywords: meta[2],
canonical: absoluteUrl(page),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [
...baseGraph(),
{ '@type': 'CollectionPage', name: meta[0], url: absoluteUrl(page), inLanguage: 'fa-IR', description: meta[1] },
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: meta[0] }]),
],
};
}
if (page.startsWith('/newsletter/')) {
const slug = page.replace('/newsletter/', '');
const issue = data.newsletterIssue;
const name = issue?.subject || 'خبرنامه تکنـاو';
const description = truncate(issue?.bodyHtml || 'نسخه‌ای از خبرنامه تکنـاو درباره روندهای مهم فناوری.', 155);
return {
title: `${name} | خبرنامه تکنـاو`,
description,
keywords: `${name}, خبرنامه تکنـاو, فناوری, هوش مصنوعی, امنیت سایبری`,
canonical: absoluteUrl(`/newsletter/${slug}`),
type: 'article',
image: DEFAULT_IMAGE,
jsonLd: [
...baseGraph(),
{
'@type': 'Article',
headline: cleanText(name).slice(0, 110),
description,
datePublished: issue?.sentAt,
dateModified: issue?.sentAt || issue?.createdAt,
inLanguage: 'fa-IR',
mainEntityOfPage: absoluteUrl(`/newsletter/${slug}`),
publisher: { '@id': `${SITE_URL}/#organization` },
},
breadcrumb([{ name: SITE_NAME, path: '/' }, { name: 'خبرنامه', path: '/newsletter' }, { name }]),
],
};
}
if (page.startsWith('/search')) {
const q = new URLSearchParams(page.split('?')[1] || '').get('q') || '';
return {
title: q ? `جستجو برای ${q} | تکناو` : 'جستجو در تکناو',
description: q ? `نتایج جستجوی تکناو برای ${q} در مقاله‌های فارسی فناوری.` : 'جستجو در مقاله‌های فارسی تکناو درباره فناوری و تکنولوژی.',
keywords: q ? `${q}, جستجوی فناوری, تکناو` : DEFAULT_KEYWORDS,
canonical: absoluteUrl(q ? `/search?q=${encodeURIComponent(q)}` : '/search'),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: baseGraph(),
};
}
return {
title: DEFAULT_TITLE,
description: DEFAULT_DESCRIPTION,
keywords: DEFAULT_KEYWORDS,
canonical: absoluteUrl('/'),
type: 'website',
image: DEFAULT_IMAGE,
jsonLd: [
...baseGraph(),
{
'@type': 'WebPage',
'@id': `${SITE_URL}/#webpage`,
name: DEFAULT_TITLE,
description: DEFAULT_DESCRIPTION,
url: absoluteUrl('/'),
inLanguage: 'fa-IR',
isPartOf: { '@id': `${SITE_URL}/#website` },
about: { '@id': `${SITE_URL}/#organization` },
speakable: {
'@type': 'SpeakableSpecification',
xpath: ['/html/head/title', '/html/head/meta[@name="description"]/@content'],
},
},
],
};
}
export function SeoManager() {
const { page } = useNav();
const [data, setData] = useState({});
useEffect(() => {
let cancelled = false;
setData({});
if (page.startsWith('/article/')) {
const rawSlug = page.replace('/article/', '');
const slug = rawSlug === 'ai-agents-2026' ? 'agentic-ai-production' : rawSlug;
const fallback = contentApi.fallbackArticles().find((a) => a.slug === slug);
if (fallback) setData({ article: fallback });
contentApi.getArticle(slug).then((res) => {
if (!cancelled && res?.article) setData({ article: res.article });
}).catch(() => {});
} else if (page.startsWith('/category/')) {
const slug = page.replace('/category/', '');
const fallback = TeknavData.categories.find((c) => c.slug === slug || c.id === slug);
if (fallback) setData({ category: fallback });
contentApi.getCategory(slug).then((category) => {
if (!cancelled && category) setData({ category });
}).catch(() => {});
} else if (page.startsWith('/author/')) {
const slug = page.replace('/author/', '');
const fallback = TeknavData.authors.find((a) => a.slug === slug);
if (fallback) setData({ author: fallback });
contentApi.getAuthor(slug).then((author) => {
if (!cancelled && author) setData({ author });
}).catch(() => {});
} else if (page.startsWith('/profile/@')) {
const username = page.replace('/profile/@', '');
api.get(`/api/profile/${username}`)
.then((res) => { if (!cancelled && res?.profile) setData({ profile: res.profile }); })
.catch(() => {});
} else if (page.startsWith('/series/')) {
const slug = page.replace('/series/', '');
contentApi.getSeries(slug)
.then((series) => { if (!cancelled && series) setData({ series }); })
.catch(() => {});
} else if (page.startsWith('/newsletter/')) {
const slug = page.replace('/newsletter/', '');
contentApi.getNewsletterIssue(slug)
.then((newsletterIssue) => { if (!cancelled && newsletterIssue) setData({ newsletterIssue }); })
.catch(() => {});
}
return () => { cancelled = true; };
}, [page]);
const seo = useMemo(() => pageSeo(page, data), [page, data]);
useEffect(() => {
const isEn = typeof window !== 'undefined' && window.location.search.includes('lang=en');
const language = seo.language || (isEn ? 'en' : 'fa-IR');
const htmlLang = language.toLocaleLowerCase('en-US').startsWith('en') ? 'en' : 'fa';
const ogLocale = htmlLang === 'en' ? 'en_US' : 'fa_IR';
const canonical = seo.canonical.split('?')[0];
document.documentElement.lang = htmlLang;
document.documentElement.dir = htmlLang === 'en' ? 'ltr' : 'rtl';
document.title = seo.title;
setMeta('name', 'description', seo.description);
setMeta('name', 'keywords', seo.keywords);
setMeta('name', 'robots', 'index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1');
setMeta('name', 'googlebot', 'index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1');
setMeta('name', 'bingbot', 'index,follow,max-snippet:-1,max-image-preview:large');
setMeta('name', 'author', SITE_NAME);
setMeta('name', 'language', language);
if (seo.academicMeta) {
Object.entries(seo.academicMeta).forEach(([name, value]) => setMeta('name', name, value));
setNamedMetaList('citation_reference', seo.citations || []);
} else {
document.head.querySelectorAll('meta[name="citation_reference"][data-teknav-seo-list="true"]').forEach((el) => el.remove());
}
setMeta('property', 'og:locale', ogLocale);
setMeta('property', 'og:site_name', SITE_NAME);
setMeta('property', 'og:type', seo.type);
setMeta('property', 'og:title', seo.title);
setMeta('property', 'og:description', seo.description);
setMeta('property', 'og:url', canonical);
setMeta('property', 'og:image', seo.image);
setMeta('property', 'og:image:width', '1200');
setMeta('property', 'og:image:height', '630');
setMeta('name', 'twitter:card', 'summary_large_image');
setMeta('name', 'twitter:title', seo.title);
setMeta('name', 'twitter:description', seo.description);
setMeta('name', 'twitter:image', seo.image);
setLink('canonical', canonical);
document.head.querySelectorAll('link[rel="alternate"][hreflang]').forEach((el) => el.remove());
setLink('alternate', canonical, { hreflang: htmlLang });
setJsonLd(seo.jsonLd);
}, [seo]);
return null;
}