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

@@ -24,6 +24,7 @@ const PricingPage = lazy(() => import('@/pages/PricingPage'));
const BlogPage = lazy(() => import('@/pages/BlogPage'));
const BlogPostPage = lazy(() => import('@/pages/BlogPostPage'));
const DevelopersPage = lazy(() => import('@/pages/DevelopersPage'));
const AllToolsPage = lazy(() => import('@/pages/AllToolsPage'));
const InternalAdminPage = lazy(() => import('@/pages/InternalAdminPage'));
const SeoRoutePage = lazy(() => import('@/pages/SeoRoutePage'));
const CookieConsent = lazy(() => import('@/components/layout/CookieConsent'));
@@ -129,6 +130,7 @@ export default function App() {
<Route path="/blog" element={<BlogPage />} />
<Route path="/blog/:slug" element={<BlogPostPage />} />
<Route path="/developers" element={<DevelopersPage />} />
<Route path="/tools" element={<AllToolsPage />} />
<Route path="/internal/admin" element={<InternalAdminPage />} />
<Route path="/ar/:slug" element={<SeoRoutePage />} />
<Route path="/:slug" element={<SeoRoutePage />} />

View File

@@ -92,6 +92,12 @@ export default function Footer() {
>
{t('common.terms')}
</Link>
<Link
to="/tools"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"
>
{t('common.allTools')}
</Link>
<Link
to="/about"
className="text-sm text-slate-500 transition-colors hover:text-primary-600 dark:text-slate-400 dark:hover:text-primary-400"

View File

@@ -1,6 +1,6 @@
import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { buildLanguageAlternates, buildSocialImageUrl, getOgLocale, getSiteOrigin } from '@/utils/seo';
import { buildSocialImageUrl, getOgLocale, getSiteOrigin } from '@/utils/seo';
const SITE_NAME = 'Dociva';
@@ -9,8 +9,6 @@ interface SEOHeadProps {
title: string;
/** Meta description */
description: string;
/** Optional keywords meta tag */
keywords?: string;
/** Canonical URL path (e.g. "/about") — origin is auto-prefixed */
path: string;
/** OG type — defaults to "website" */
@@ -24,19 +22,19 @@ interface SEOHeadProps {
/**
* Reusable SEO head component that injects:
* - title, description, canonical URL
* - optional keywords meta tag
* - OpenGraph meta tags (title, description, url, type, site_name, locale)
* - Twitter card meta tags
* - Optional JSON-LD structured data
*/
export default function SEOHead({ title, description, keywords, path, type = 'website', jsonLd, alternates }: SEOHeadProps) {
export default function SEOHead({ title, description, path, type = 'website', jsonLd, alternates }: SEOHeadProps) {
const { i18n } = useTranslation();
const origin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
const canonicalUrl = `${origin}${path}`;
const socialImageUrl = buildSocialImageUrl(origin);
const fullTitle = `${title}${SITE_NAME}`;
const languageAlternates = alternates ?? buildLanguageAlternates(origin, path);
const languageAlternates = alternates ?? [];
const currentOgLocale = getOgLocale(i18n.language);
const xDefaultHref = languageAlternates.find((alternate) => alternate.hrefLang === 'en')?.href ?? canonicalUrl;
const schemas = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : [];
@@ -45,7 +43,8 @@ export default function SEOHead({ title, description, keywords, path, type = 'we
<title>{fullTitle}</title>
<meta name="description" content={description} />
<meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1" />
{keywords ? <meta name="keywords" content={keywords} /> : null}
<meta name="application-name" content={SITE_NAME} />
<meta name="apple-mobile-web-app-title" content={SITE_NAME} />
<link rel="canonical" href={canonicalUrl} />
{languageAlternates.map((alternate) => (
<link
@@ -55,7 +54,7 @@ export default function SEOHead({ title, description, keywords, path, type = 'we
href={alternate.href}
/>
))}
<link rel="alternate" hrefLang="x-default" href={canonicalUrl} />
<link rel="alternate" hrefLang="x-default" href={xDefaultHref} />
{/* OpenGraph */}
<meta property="og:title" content={fullTitle} />
@@ -64,6 +63,7 @@ export default function SEOHead({ title, description, keywords, path, type = 'we
<meta property="og:type" content={type} />
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:image" content={socialImageUrl} />
<meta property="og:image:type" content="image/svg+xml" />
<meta property="og:image:alt" content={`${fullTitle} social preview`} />
<meta property="og:locale" content={currentOgLocale} />
{languageAlternates

View File

@@ -2,7 +2,7 @@ import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { CheckCircle } from 'lucide-react';
import { getToolSEO } from '@/config/seoData';
import { buildLanguageAlternates, buildSocialImageUrl, generateToolSchema, generateBreadcrumbs, generateFAQ, generateHowTo, getOgLocale, getSiteOrigin } from '@/utils/seo';
import { buildSocialImageUrl, generateToolSchema, generateBreadcrumbs, generateFAQ, generateHowTo, getOgLocale, getSiteOrigin } from '@/utils/seo';
import BreadcrumbNav from './BreadcrumbNav';
import FAQSection from './FAQSection';
import RelatedTools from './RelatedTools';
@@ -43,7 +43,6 @@ export default function ToolLandingPage({ slug, children }: ToolLandingPageProps
const path = `/tools/${slug}`;
const canonicalUrl = `${origin}${path}`;
const socialImageUrl = buildSocialImageUrl(origin);
const languageAlternates = buildLanguageAlternates(origin, path);
const currentOgLocale = getOgLocale(i18n.language);
const toolSchema = generateToolSchema({
@@ -77,18 +76,8 @@ export default function ToolLandingPage({ slug, children }: ToolLandingPageProps
<Helmet>
<title>{toolTitle} {seo.titleSuffix} | {t('common.appName')}</title>
<meta name="description" content={seo.metaDescription} />
<meta name="keywords" content={seo.keywords} />
<meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1" />
<link rel="canonical" href={canonicalUrl} />
{languageAlternates.map((alternate) => (
<link
key={alternate.hrefLang}
rel="alternate"
hrefLang={alternate.hrefLang}
href={alternate.href}
/>
))}
<link rel="alternate" hrefLang="x-default" href={canonicalUrl} />
{/* Open Graph */}
<meta property="og:title" content={`${toolTitle}${seo.titleSuffix}`} />
@@ -98,11 +87,6 @@ export default function ToolLandingPage({ slug, children }: ToolLandingPageProps
<meta property="og:image" content={socialImageUrl} />
<meta property="og:image:alt" content={`${toolTitle} social preview`} />
<meta property="og:locale" content={currentOgLocale} />
{languageAlternates
.filter((alternate) => alternate.ogLocale !== currentOgLocale)
.map((alternate) => (
<meta key={alternate.ogLocale} property="og:locale:alternate" content={alternate.ogLocale} />
))}
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />

View File

@@ -21,6 +21,7 @@ const STATIC_PAGE_ROUTES = [
'/blog',
'/blog/:slug',
'/developers',
'/tools',
'/internal/admin',
] as const;

View File

@@ -256,6 +256,19 @@
"contactTitle": "8. الاتصال",
"contactText": "أسئلة حول هذه الشروط؟ تواصل معنا على"
},
"toolsHub": {
"metaTitle": "كل الأدوات",
"metaDescription": "تصفح جميع أدوات Dociva في مكان واحد. استكشف مسارات PDF والصور والذكاء الاصطناعي والتحويل والأدوات المساعدة من دليل واحد سهل للأرشفة والزحف.",
"title": "جميع أدوات Dociva",
"description": "استخدم هذا الدليل لاستكشاف كل مسارات Dociva حسب الفئة ثم انتقل مباشرة إلى الأداة التي تحتاجها.",
"categories": {
"PDF": "أدوات PDF",
"Convert": "أدوات التحويل",
"Image": "أدوات الصور",
"AI": "أدوات الذكاء الاصطناعي",
"Utility": "أدوات مساعدة"
}
},
"cookie": {
"title": "إعدادات ملفات الارتباط",
"message": "نستخدم ملفات الارتباط لتحسين تجربتك وتحليل حركة الموقع. بالموافقة، فإنك توافق على ملفات الارتباط التحليلية.",

View File

@@ -256,6 +256,19 @@
"contactTitle": "8. Contact",
"contactText": "Questions about these terms? Contact us at"
},
"toolsHub": {
"metaTitle": "All Tools",
"metaDescription": "Browse every Dociva tool in one place. Explore PDF, image, AI, conversion, and utility workflows from a single search-friendly directory.",
"title": "All Dociva Tools",
"description": "Use this directory to explore every Dociva workflow by category and jump directly to the tool you need.",
"categories": {
"PDF": "PDF Tools",
"Convert": "Convert Tools",
"Image": "Image Tools",
"AI": "AI Tools",
"Utility": "Utility Tools"
}
},
"cookie": {
"title": "Cookie Settings",
"message": "We use cookies to improve your experience and analyze site traffic. By accepting, you consent to analytics cookies.",

View File

@@ -256,6 +256,19 @@
"contactTitle": "8. Contact",
"contactText": "Des questions sur ces conditions ? Contactez-nous à"
},
"toolsHub": {
"metaTitle": "Tous les outils",
"metaDescription": "Parcourez tous les outils Dociva depuis une seule page. Explorez les workflows PDF, image, IA, conversion et utilitaires dans un répertoire clair et optimisé pour la découverte.",
"title": "Tous les outils Dociva",
"description": "Utilisez ce répertoire pour parcourir chaque workflow Dociva par catégorie et ouvrir directement l'outil dont vous avez besoin.",
"categories": {
"PDF": "Outils PDF",
"Convert": "Outils de conversion",
"Image": "Outils d'image",
"AI": "Outils IA",
"Utility": "Outils utilitaires"
}
},
"cookie": {
"title": "Paramètres des cookies",
"message": "Nous utilisons des cookies pour améliorer votre expérience et analyser le trafic du site. En acceptant, vous consentez aux cookies analytiques.",

View File

@@ -0,0 +1,99 @@
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import SEOHead from '@/components/seo/SEOHead';
import BreadcrumbNav from '@/components/seo/BreadcrumbNav';
import { TOOLS_SEO } from '@/config/seoData';
import { generateBreadcrumbs, generateCollectionPage, generateItemList, getSiteOrigin } from '@/utils/seo';
const CATEGORY_ORDER = ['PDF', 'Convert', 'Image', 'AI', 'Utility'] as const;
export default function AllToolsPage() {
const { t } = useTranslation();
const origin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
const path = '/tools';
const url = `${origin}${path}`;
const groupedTools = CATEGORY_ORDER.map((category) => ({
category,
items: TOOLS_SEO.filter((tool) => tool.category === category),
})).filter((group) => group.items.length > 0);
const jsonLd = [
generateCollectionPage({
name: t('pages.toolsHub.metaTitle'),
description: t('pages.toolsHub.metaDescription'),
url,
}),
generateBreadcrumbs([
{ name: t('common.home'), url: origin },
{ name: t('common.allTools'), url },
]),
generateItemList(
TOOLS_SEO.map((tool) => ({
name: t(`tools.${tool.i18nKey}.title`),
url: `${origin}/tools/${tool.slug}`,
})),
),
];
return (
<>
<SEOHead
title={t('pages.toolsHub.metaTitle')}
description={t('pages.toolsHub.metaDescription')}
path={path}
jsonLd={jsonLd}
/>
<div className="mx-auto max-w-6xl space-y-10">
<section className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 sm:p-10">
<BreadcrumbNav
className="mb-6"
items={[
{ label: t('common.home'), to: '/' },
{ label: t('common.allTools') },
]}
/>
<h1 className="text-3xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-4xl">
{t('pages.toolsHub.title')}
</h1>
<p className="mt-4 max-w-3xl text-lg leading-8 text-slate-600 dark:text-slate-400">
{t('pages.toolsHub.description')}
</p>
</section>
{groupedTools.map((group) => (
<section
key={group.category}
className="rounded-2xl border border-slate-200 bg-white p-7 shadow-sm dark:border-slate-700 dark:bg-slate-900/70"
>
<h2 className="text-2xl font-semibold text-slate-900 dark:text-white">
{t(`pages.toolsHub.categories.${group.category}`)}
</h2>
<div className="mt-5 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{group.items.map((tool) => (
<Link
key={tool.slug}
to={`/tools/${tool.slug}`}
className="rounded-2xl border border-slate-200 p-5 transition-colors hover:border-primary-300 hover:bg-slate-50 dark:border-slate-700 dark:hover:border-primary-600 dark:hover:bg-slate-800"
>
<p className="text-sm font-medium uppercase tracking-wide text-primary-600 dark:text-primary-400">
{group.category}
</p>
<h3 className="mt-2 text-lg font-semibold text-slate-900 dark:text-white">
{t(`tools.${tool.i18nKey}.title`)}
</h3>
<p className="mt-2 text-sm leading-6 text-slate-600 dark:text-slate-400">
{t(`tools.${tool.i18nKey}.shortDesc`)}
</p>
</Link>
))}
</div>
</section>
))}
</div>
</>
);
}

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
import SEOHead from '@/components/seo/SEOHead';
import { generateWebPage, getSiteOrigin } from '@/utils/seo';
import { generateCollectionPage, generateItemList, getSiteOrigin } from '@/utils/seo';
import { BookOpen, Calendar, ArrowRight, Search, X } from 'lucide-react';
import {
BLOG_ARTICLES,
@@ -44,11 +44,17 @@ export default function BlogPage() {
title={t('pages.blog.metaTitle')}
description={t('pages.blog.metaDescription')}
path="/blog"
jsonLd={generateWebPage({
name: t('pages.blog.metaTitle'),
description: t('pages.blog.metaDescription'),
url: `${siteOrigin}/blog`,
})}
jsonLd={[
generateCollectionPage({
name: t('pages.blog.metaTitle'),
description: t('pages.blog.metaDescription'),
url: `${siteOrigin}/blog`,
}),
generateItemList(posts.map((post) => ({
name: post.title,
url: `${siteOrigin}/blog/${post.slug}`,
}))),
]}
/>
<div className="mx-auto max-w-4xl">

View File

@@ -2,7 +2,7 @@ import { useDeferredValue } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router-dom';
import SEOHead from '@/components/seo/SEOHead';
import { generateOrganization, getSiteOrigin } from '@/utils/seo';
import { generateOrganization, generateWebSite, getSiteOrigin } from '@/utils/seo';
import {
FileText,
FileOutput,
@@ -121,18 +121,10 @@ export default function HomePage() {
description={t('home.heroSub')}
path="/"
jsonLd={[
{
'@context': 'https://schema.org',
'@type': 'WebSite',
name: t('common.appName'),
url: siteOrigin,
generateWebSite({
origin: siteOrigin,
description: t('home.heroSub'),
potentialAction: {
'@type': 'SearchAction',
target: `${siteOrigin}/?q={search_term_string}`,
'query-input': 'required name=search_term_string',
},
},
}),
generateOrganization(siteOrigin),
]}
/>

View File

@@ -6,13 +6,19 @@ import SEOHead from '@/components/seo/SEOHead';
import FAQSection from '@/components/seo/FAQSection';
import {
getLocalizedText,
getLocalizedTextList,
getSeoCollectionPage,
interpolateTemplate,
normalizeSeoLocale,
} from '@/config/seoPages';
import { getToolSEO } from '@/config/seoData';
import { generateBreadcrumbs, generateFAQ, generateWebPage, getSiteOrigin } from '@/utils/seo';
import {
generateBreadcrumbs,
generateCollectionPage,
generateFAQ,
generateItemList,
generateWebPage,
getSiteOrigin,
} from '@/utils/seo';
import NotFoundPage from '@/pages/NotFoundPage';
interface SeoCollectionPageProps {
@@ -64,7 +70,6 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
const title = interpolateTemplate(getLocalizedText(page.titleTemplate, locale), tokens);
const description = interpolateTemplate(getLocalizedText(page.descriptionTemplate, locale), tokens);
const intro = interpolateTemplate(getLocalizedText(page.introTemplate, locale), tokens);
const keywords = [focusKeyword, ...getLocalizedTextList(page.supportingKeywords, locale)].join(', ');
const siteOrigin = getSiteOrigin(typeof window !== 'undefined' ? window.location.origin : '');
const faqItems = page.faqTemplates.map((item) => ({
question: getLocalizedText(item.question, locale),
@@ -82,6 +87,11 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
];
const jsonLd = [
generateCollectionPage({
name: title,
description,
url,
}),
generateWebPage({
name: title,
description,
@@ -93,11 +103,18 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
{ name: title, url },
]),
generateFAQ(faqItems),
generateItemList(page.targetToolSlugs.map((toolSlug) => {
const tool = getToolSEO(toolSlug);
return {
name: tool ? t(`tools.${tool.i18nKey}.title`) : toolSlug,
url: `${siteOrigin}/tools/${toolSlug}`,
};
})),
];
return (
<>
<SEOHead title={title} description={description} path={path} keywords={keywords} jsonLd={jsonLd} alternates={alternates} />
<SEOHead title={title} description={description} path={path} jsonLd={jsonLd} alternates={alternates} />
<div className="mx-auto max-w-6xl space-y-10">
<section className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 sm:p-10">
@@ -227,4 +244,4 @@ export default function SeoCollectionPage({ slug }: SeoCollectionPageProps) {
</div>
</>
);
}
}

View File

@@ -8,7 +8,6 @@ import RelatedTools from '@/components/seo/RelatedTools';
import SuggestedTools from '@/components/seo/SuggestedTools';
import {
getLocalizedText,
getLocalizedTextList,
getProgrammaticToolPage,
getSeoCollectionPage,
interpolateTemplate,
@@ -19,6 +18,7 @@ import {
generateBreadcrumbs,
generateFAQ,
generateHowTo,
generateItemList,
generateToolSchema,
generateWebPage,
getSiteOrigin,
@@ -82,7 +82,6 @@ export default function SeoPage({ slug }: SeoPageProps) {
const useCases = t(`seo.${tool.i18nKey}.useCases`, { returnObjects: true }) as string[];
const focusKeyword = getLocalizedText(page.focusKeyword, locale);
const keywords = [focusKeyword, ...getLocalizedTextList(page.supportingKeywords, locale)].join(', ');
const tokens = {
brand: 'Dociva',
focusKeyword,
@@ -139,11 +138,25 @@ export default function SeoPage({ slug }: SeoPageProps) {
url,
}),
generateFAQ(faqItems),
generateItemList(page.relatedCollectionSlugs.map((collectionSlug) => {
const collection = getSeoCollectionPage(collectionSlug);
const collectionTitle = collection
? interpolateTemplate(getLocalizedText(collection.titleTemplate, locale), {
brand: 'Dociva',
focusKeyword: getLocalizedText(collection.focusKeyword, locale),
})
: collectionSlug;
return {
name: collectionTitle,
url: `${siteOrigin}${localizedCollectionPath(collectionSlug)}`,
};
})),
];
return (
<>
<SEOHead title={title} description={description} path={path} keywords={keywords} jsonLd={jsonLd} alternates={alternates} />
<SEOHead title={title} description={description} path={path} jsonLd={jsonLd} alternates={alternates} />
<div className="mx-auto max-w-6xl space-y-12">
<section className="rounded-[2rem] border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-700 dark:bg-slate-900/70 sm:p-10">
@@ -312,4 +325,4 @@ export default function SeoPage({ slug }: SeoPageProps) {
</div>
</>
);
}
}

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