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:
Your Name
2026-03-30 10:31:27 +02:00
parent 4ac4bf4e42
commit 736d08ef04
24 changed files with 2030 additions and 1549 deletions

View File

@@ -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,
};
}