- Introduced seoPages.ts to manage SEO-related configurations and types for programmatic tools and collection pages. - Created SeoCollectionPage and SeoProgrammaticPage components to render SEO content dynamically based on the new configuration. - Enhanced API service to ensure CSRF token handling for secure requests. - Added generateHowTo utility function for structured data generation. - Updated sitemap generation script to include SEO tool and collection pages. - Configured TypeScript to resolve JSON modules for easier integration of SEO data. ستراتيجية التنفيذ لم أغير أي core logic في أدوات التحويل أو الضغط أو التحرير استخدمت architecture إضافية فوق النظام الحالي بدل استبداله جعلت الـ SEO pages تعتمد على source of truth واحد حتى يسهل التوسع ربطت التوليد مع build حتى لا تبقى sitemap وrobots ثابتة أو منسية دعمت العربية والإنجليزية داخل نفس config الجديد عززت internal linking من: صفحات SEO إلى tool pages صفحات SEO إلى collection pages footer إلى collection pages Suggested tools داخل صفحات الأدوات التحقق
225 lines
5.3 KiB
TypeScript
225 lines
5.3 KiB
TypeScript
/**
|
|
* SEO utility functions for structured data generation.
|
|
*/
|
|
|
|
export interface ToolSeoData {
|
|
name: string;
|
|
description: string;
|
|
url: string;
|
|
category?: string;
|
|
ratingValue?: number;
|
|
ratingCount?: number;
|
|
}
|
|
|
|
export interface LanguageAlternate {
|
|
hrefLang: string;
|
|
href: string;
|
|
ogLocale: string;
|
|
}
|
|
|
|
const DEFAULT_SOCIAL_IMAGE_PATH = '/social-preview.svg';
|
|
const DEFAULT_SITE_ORIGIN = 'https://dociva.io';
|
|
|
|
const LANGUAGE_CONFIG: Record<'en' | 'ar' | 'fr', { hrefLang: string; ogLocale: string }> = {
|
|
en: { hrefLang: 'en', ogLocale: 'en_US' },
|
|
ar: { hrefLang: 'ar', ogLocale: 'ar_SA' },
|
|
fr: { hrefLang: 'fr', ogLocale: 'fr_FR' },
|
|
};
|
|
|
|
export function normalizeSiteLanguage(language: string): 'en' | 'ar' | 'fr' {
|
|
const baseLanguage = language.split('-')[0];
|
|
return baseLanguage === 'ar' || baseLanguage === 'fr' ? baseLanguage : 'en';
|
|
}
|
|
|
|
export function getOgLocale(language: string): string {
|
|
return LANGUAGE_CONFIG[normalizeSiteLanguage(language)].ogLocale;
|
|
}
|
|
|
|
export function buildLanguageAlternates(origin: string, path: string): LanguageAlternate[] {
|
|
const separator = path.includes('?') ? '&' : '?';
|
|
return (Object.entries(LANGUAGE_CONFIG) as Array<[keyof typeof LANGUAGE_CONFIG, (typeof LANGUAGE_CONFIG)[keyof typeof LANGUAGE_CONFIG]]>)
|
|
.map(([language, config]) => ({
|
|
hrefLang: config.hrefLang,
|
|
href: `${origin}${path}${separator}lng=${language}`,
|
|
ogLocale: config.ogLocale,
|
|
}));
|
|
}
|
|
|
|
export function getSiteOrigin(currentOrigin = ''): string {
|
|
const configuredOrigin = String(import.meta.env.VITE_SITE_DOMAIN || '').trim().replace(/\/$/, '');
|
|
if (configuredOrigin) {
|
|
return configuredOrigin;
|
|
}
|
|
|
|
if (currentOrigin) {
|
|
return currentOrigin.replace(/\/$/, '');
|
|
}
|
|
|
|
return DEFAULT_SITE_ORIGIN;
|
|
}
|
|
|
|
export function buildSocialImageUrl(origin: string): string {
|
|
return `${origin}${DEFAULT_SOCIAL_IMAGE_PATH}`;
|
|
}
|
|
|
|
/**
|
|
* Generate WebApplication JSON-LD structured data for a tool page.
|
|
*/
|
|
export function generateToolSchema(tool: ToolSeoData): object {
|
|
const schema: Record<string, unknown> = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebApplication',
|
|
name: tool.name,
|
|
url: tool.url,
|
|
applicationCategory: tool.category || 'UtilitiesApplication',
|
|
operatingSystem: 'Any',
|
|
offers: {
|
|
'@type': 'Offer',
|
|
price: '0',
|
|
priceCurrency: 'USD',
|
|
},
|
|
description: tool.description,
|
|
inLanguage: ['en', 'ar', 'fr'],
|
|
};
|
|
|
|
if (tool.ratingValue && tool.ratingCount && tool.ratingCount > 0) {
|
|
schema.aggregateRating = {
|
|
'@type': 'AggregateRating',
|
|
ratingValue: tool.ratingValue,
|
|
ratingCount: tool.ratingCount,
|
|
bestRating: 5,
|
|
worstRating: 1,
|
|
};
|
|
}
|
|
|
|
return schema;
|
|
}
|
|
|
|
/**
|
|
* Generate BreadcrumbList JSON-LD.
|
|
*/
|
|
export function generateBreadcrumbs(
|
|
items: { name: string; url: string }[]
|
|
): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BreadcrumbList',
|
|
itemListElement: items.map((item, index) => ({
|
|
'@type': 'ListItem',
|
|
position: index + 1,
|
|
name: item.name,
|
|
item: item.url,
|
|
})),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate FAQ structured data.
|
|
*/
|
|
export function generateFAQ(
|
|
questions: { question: string; answer: string }[]
|
|
): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'FAQPage',
|
|
mainEntity: questions.map((q) => ({
|
|
'@type': 'Question',
|
|
name: q.question,
|
|
acceptedAnswer: {
|
|
'@type': 'Answer',
|
|
text: q.answer,
|
|
},
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function generateHowTo(data: {
|
|
name: string;
|
|
description: string;
|
|
steps: string[];
|
|
url: string;
|
|
}): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'HowTo',
|
|
name: data.name,
|
|
description: data.description,
|
|
url: data.url,
|
|
step: data.steps.map((text, index) => ({
|
|
'@type': 'HowToStep',
|
|
position: index + 1,
|
|
name: text,
|
|
text,
|
|
})),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate Organization JSON-LD for the site.
|
|
*/
|
|
export function generateOrganization(origin: string): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Organization',
|
|
name: 'Dociva',
|
|
url: origin,
|
|
logo: `${origin}/favicon.svg`,
|
|
sameAs: [],
|
|
contactPoint: {
|
|
'@type': 'ContactPoint',
|
|
email: 'support@dociva.io',
|
|
contactType: 'customer support',
|
|
availableLanguage: ['English', 'Arabic', 'French'],
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate WebPage JSON-LD for a static page.
|
|
*/
|
|
export function generateWebPage(page: {
|
|
name: string;
|
|
description: string;
|
|
url: string;
|
|
}): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'WebPage',
|
|
name: page.name,
|
|
description: page.description,
|
|
url: page.url,
|
|
isPartOf: {
|
|
'@type': 'WebSite',
|
|
name: 'Dociva',
|
|
},
|
|
};
|
|
}
|
|
|
|
export function generateBlogPosting(post: {
|
|
headline: string;
|
|
description: string;
|
|
url: string;
|
|
datePublished: string;
|
|
inLanguage: string;
|
|
}): object {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'BlogPosting',
|
|
headline: post.headline,
|
|
description: post.description,
|
|
url: post.url,
|
|
datePublished: post.datePublished,
|
|
dateModified: post.datePublished,
|
|
inLanguage: post.inLanguage,
|
|
author: {
|
|
'@type': 'Organization',
|
|
name: 'Dociva',
|
|
},
|
|
publisher: {
|
|
'@type': 'Organization',
|
|
name: 'Dociva',
|
|
},
|
|
mainEntityOfPage: post.url,
|
|
};
|
|
}
|