diff --git a/app/src/lib/seo.jsx b/app/src/lib/seo.jsx new file mode 100644 index 0000000..612a8c0 --- /dev/null +++ b/app/src/lib/seo.jsx @@ -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>/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; +}