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')}