diff --git a/frontend/index.html b/frontend/index.html index deea0dd..860d3e4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,10 +1,13 @@ - + - frontend + Ombrora Convert + + +
diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..cf5a310 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://convert.ombrora.com/sitemap.xml diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml new file mode 100644 index 0000000..11bf271 --- /dev/null +++ b/frontend/public/sitemap.xml @@ -0,0 +1,9 @@ + + + + https://convert.ombrora.com/fr/ + + + https://convert.ombrora.com/en/ + + diff --git a/frontend/src/components/SeoHead.jsx b/frontend/src/components/SeoHead.jsx new file mode 100644 index 0000000..c5c8f9b --- /dev/null +++ b/frontend/src/components/SeoHead.jsx @@ -0,0 +1,88 @@ +import { useEffect } from 'react'; + +const SITE_URL = 'https://convert.ombrora.com'; + +const META = { + fr: { + title: 'Ombrora Convert — Conversion de fichiers en ligne', + description: + "Convertissez vos fichiers en ligne gratuitement : images, documents, polices et ebooks. Aucun compte requis, fichiers supprimés automatiquement.", + }, + en: { + title: 'Ombrora Convert — Online File Conversion', + description: + 'Convert your files online for free: images, documents, fonts, and ebooks. No account required, files deleted automatically.', + }, +}; + +function setNameMeta(name, content) { + let el = document.head.querySelector(`meta[name="${name}"]`); + if (!el) { + el = document.createElement('meta'); + el.setAttribute('name', name); + document.head.appendChild(el); + } + el.setAttribute('content', content); +} + +function setPropertyMeta(property, content) { + let el = document.head.querySelector(`meta[property="${property}"]`); + if (!el) { + el = document.createElement('meta'); + el.setAttribute('property', property); + document.head.appendChild(el); + } + el.setAttribute('content', content); +} + +function setAlternateLink(hreflang, href) { + const selector = `link[rel="alternate"][hreflang="${hreflang}"]`; + let el = document.head.querySelector(selector); + if (!el) { + el = document.createElement('link'); + el.setAttribute('rel', 'alternate'); + el.setAttribute('hreflang', hreflang); + document.head.appendChild(el); + } + el.setAttribute('href', href); +} + +function setJsonLd(lang) { + const id = 'seo-jsonld'; + let script = document.getElementById(id); + if (!script) { + script = document.createElement('script'); + script.id = id; + script.type = 'application/ld+json'; + document.head.appendChild(script); + } + script.textContent = JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'Ombrora Convert', + applicationCategory: 'UtilitiesApplication', + operatingSystem: 'Any', + offers: { '@type': 'Offer', price: '0', priceCurrency: 'EUR' }, + inLanguage: lang, + url: `${SITE_URL}/${lang}/`, + }); +} + +export function SeoHead({ lang }) { + useEffect(() => { + const meta = META[lang] ?? META.fr; + document.title = meta.title; + document.documentElement.lang = lang; + setNameMeta('description', meta.description); + setPropertyMeta('og:title', meta.title); + setPropertyMeta('og:description', meta.description); + setPropertyMeta('og:type', 'website'); + setPropertyMeta('og:locale', lang === 'fr' ? 'fr_FR' : 'en_US'); + setAlternateLink('fr', `${SITE_URL}/fr/`); + setAlternateLink('en', `${SITE_URL}/en/`); + setAlternateLink('x-default', `${SITE_URL}/fr/`); + setJsonLd(lang); + }, [lang]); + + return null; +} diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx index 40102c8..0b11e4e 100644 --- a/frontend/src/pages/HomePage.jsx +++ b/frontend/src/pages/HomePage.jsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { fetchFormats, uploadFiles } from '../api.js'; import { FileCard } from '../FileCard.jsx'; @@ -6,6 +7,7 @@ import { Dropzone } from '../components/Dropzone.jsx'; import { FileConfigCard } from '../components/FileConfigCard.jsx'; import { ReassuranceStrip } from '../components/ReassuranceStrip.jsx'; import { FormatsGrid } from '../components/FormatsGrid.jsx'; +import { SeoHead } from '../components/SeoHead.jsx'; import '../styles/home.css'; import '../styles/sections.css'; @@ -29,6 +31,7 @@ function defaultQualityFor(targetFormat) { } export function HomePage() { + const { lang } = useParams(); const { t } = useTranslation(); const [pendingFiles, setPendingFiles] = useState([]); const [submittedJobs, setSubmittedJobs] = useState([]); @@ -83,6 +86,7 @@ export function HomePage() { return ( <> +

{t('hero.title')}

{t('hero.subtitle')}