feat: enhance SEO capabilities and add All Tools page
- Updated generate-seo-assets script to create separate sitemap files for static, blog, tools, and SEO pages. - Introduced render-seo-shells script to generate HTML shells for SEO pages with dynamic metadata. - Added All Tools page with categorized tool listings and SEO metadata. - Updated routing to include /tools path and linked it in the footer. - Enhanced SEOHead component to remove unused keywords and improve OpenGraph metadata. - Updated translations for tools hub in English, Arabic, and French. - Refactored SEO-related utility functions to support new structured data formats.
This commit is contained in:
@@ -9,6 +9,7 @@ export interface ToolSeoData {
|
||||
category?: string;
|
||||
ratingValue?: number;
|
||||
ratingCount?: number;
|
||||
features?: string[];
|
||||
}
|
||||
|
||||
export interface LanguageAlternate {
|
||||
@@ -19,6 +20,7 @@ export interface LanguageAlternate {
|
||||
|
||||
const DEFAULT_SOCIAL_IMAGE_PATH = '/social-preview.svg';
|
||||
const DEFAULT_SITE_ORIGIN = 'https://dociva.io';
|
||||
const DEFAULT_SITE_NAME = 'Dociva';
|
||||
|
||||
const LANGUAGE_CONFIG: Record<'en' | 'ar' | 'fr', { hrefLang: string; ogLocale: string }> = {
|
||||
en: { hrefLang: 'en', ogLocale: 'en_US' },
|
||||
@@ -35,13 +37,16 @@ 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 buildLanguageAlternates(
|
||||
origin: string,
|
||||
localizedPaths: Partial<Record<'en' | 'ar' | 'fr', string>>,
|
||||
): LanguageAlternate[] {
|
||||
return (Object.entries(localizedPaths) as Array<[keyof typeof LANGUAGE_CONFIG, string | undefined]>)
|
||||
.filter(([, path]) => Boolean(path))
|
||||
.map(([language, path]) => ({
|
||||
hrefLang: LANGUAGE_CONFIG[language].hrefLang,
|
||||
href: `${origin}${path}`,
|
||||
ogLocale: LANGUAGE_CONFIG[language].ogLocale,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -68,20 +73,33 @@ export function buildSocialImageUrl(origin: string): string {
|
||||
export function generateToolSchema(tool: ToolSeoData): object {
|
||||
const schema: Record<string, unknown> = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebApplication',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: tool.name,
|
||||
url: tool.url,
|
||||
applicationCategory: tool.category || 'UtilitiesApplication',
|
||||
applicationSubCategory: tool.category || 'UtilitiesApplication',
|
||||
operatingSystem: 'Any',
|
||||
browserRequirements: 'Requires JavaScript. Works in modern browsers.',
|
||||
isAccessibleForFree: true,
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
availability: 'https://schema.org/InStock',
|
||||
},
|
||||
description: tool.description,
|
||||
inLanguage: ['en', 'ar', 'fr'],
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: DEFAULT_SITE_NAME,
|
||||
url: getSiteOrigin(),
|
||||
},
|
||||
};
|
||||
|
||||
if (tool.features && tool.features.length > 0) {
|
||||
schema.featureList = tool.features;
|
||||
}
|
||||
|
||||
if (tool.ratingValue && tool.ratingCount && tool.ratingCount > 0) {
|
||||
schema.aggregateRating = {
|
||||
'@type': 'AggregateRating',
|
||||
@@ -161,10 +179,14 @@ export function generateOrganization(origin: string): object {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: 'Dociva',
|
||||
'@id': `${origin}/#organization`,
|
||||
name: DEFAULT_SITE_NAME,
|
||||
alternateName: 'Dociva File Tools',
|
||||
url: origin,
|
||||
logo: `${origin}/favicon.svg`,
|
||||
sameAs: [],
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: `${origin}/logo.svg`,
|
||||
},
|
||||
contactPoint: {
|
||||
'@type': 'ContactPoint',
|
||||
email: 'support@dociva.io',
|
||||
@@ -188,13 +210,68 @@ export function generateWebPage(page: {
|
||||
name: page.name,
|
||||
description: page.description,
|
||||
url: page.url,
|
||||
inLanguage: ['en', 'ar', 'fr'],
|
||||
isPartOf: {
|
||||
'@type': 'WebSite',
|
||||
name: 'Dociva',
|
||||
'@id': `${getSiteOrigin()}/#website`,
|
||||
name: DEFAULT_SITE_NAME,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function generateWebSite(data: {
|
||||
origin: string;
|
||||
description: string;
|
||||
}): object {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
'@id': `${data.origin}/#website`,
|
||||
name: DEFAULT_SITE_NAME,
|
||||
url: data.origin,
|
||||
description: data.description,
|
||||
publisher: {
|
||||
'@id': `${data.origin}/#organization`,
|
||||
},
|
||||
inLanguage: ['en', 'ar', 'fr'],
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: `${data.origin}/?q={search_term_string}`,
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function generateCollectionPage(data: {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
}): object {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
url: data.url,
|
||||
isPartOf: {
|
||||
'@id': `${getSiteOrigin()}/#website`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function generateItemList(items: { name: string; url: string }[]): object {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ItemList',
|
||||
itemListElement: items.map((item, index) => ({
|
||||
'@type': 'ListItem',
|
||||
position: index + 1,
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function generateBlogPosting(post: {
|
||||
headline: string;
|
||||
description: string;
|
||||
@@ -202,6 +279,7 @@ export function generateBlogPosting(post: {
|
||||
datePublished: string;
|
||||
inLanguage: string;
|
||||
}): object {
|
||||
const origin = getSiteOrigin();
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
@@ -211,14 +289,23 @@ export function generateBlogPosting(post: {
|
||||
datePublished: post.datePublished,
|
||||
dateModified: post.datePublished,
|
||||
inLanguage: post.inLanguage,
|
||||
isAccessibleForFree: true,
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
name: 'Dociva',
|
||||
name: DEFAULT_SITE_NAME,
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'Dociva',
|
||||
'@id': `${origin}/#organization`,
|
||||
name: DEFAULT_SITE_NAME,
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: `${origin}/logo.svg`,
|
||||
},
|
||||
},
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': post.url,
|
||||
},
|
||||
mainEntityOfPage: post.url,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user