feat(frontend): add per-language SEO metadata, JSON-LD, robots.txt, and sitemap

Part of the Ombrora Convert redesign (Task 4/5).
This commit is contained in:
2026-07-31 14:59:06 +02:00
parent 2aa8e51b80
commit 8e405bd3b0
5 changed files with 110 additions and 2 deletions
+5 -2
View File
@@ -1,10 +1,13 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="fr">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title> <title>Ombrora Convert</title>
<meta name="description" content="Convertissez vos fichiers en ligne gratuitement : images, documents, polices et ebooks." />
<meta property="og:site_name" content="Ombrora Convert" />
<meta name="twitter:card" content="summary" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://convert.ombrora.com/sitemap.xml
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://convert.ombrora.com/fr/</loc>
</url>
<url>
<loc>https://convert.ombrora.com/en/</loc>
</url>
</urlset>
+88
View File
@@ -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;
}
+4
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { fetchFormats, uploadFiles } from '../api.js'; import { fetchFormats, uploadFiles } from '../api.js';
import { FileCard } from '../FileCard.jsx'; import { FileCard } from '../FileCard.jsx';
@@ -6,6 +7,7 @@ import { Dropzone } from '../components/Dropzone.jsx';
import { FileConfigCard } from '../components/FileConfigCard.jsx'; import { FileConfigCard } from '../components/FileConfigCard.jsx';
import { ReassuranceStrip } from '../components/ReassuranceStrip.jsx'; import { ReassuranceStrip } from '../components/ReassuranceStrip.jsx';
import { FormatsGrid } from '../components/FormatsGrid.jsx'; import { FormatsGrid } from '../components/FormatsGrid.jsx';
import { SeoHead } from '../components/SeoHead.jsx';
import '../styles/home.css'; import '../styles/home.css';
import '../styles/sections.css'; import '../styles/sections.css';
@@ -29,6 +31,7 @@ function defaultQualityFor(targetFormat) {
} }
export function HomePage() { export function HomePage() {
const { lang } = useParams();
const { t } = useTranslation(); const { t } = useTranslation();
const [pendingFiles, setPendingFiles] = useState([]); const [pendingFiles, setPendingFiles] = useState([]);
const [submittedJobs, setSubmittedJobs] = useState([]); const [submittedJobs, setSubmittedJobs] = useState([]);
@@ -83,6 +86,7 @@ export function HomePage() {
return ( return (
<> <>
<SeoHead lang={lang} />
<section className="hero"> <section className="hero">
<h1>{t('hero.title')}</h1> <h1>{t('hero.title')}</h1>
<p>{t('hero.subtitle')}</p> <p>{t('hero.subtitle')}</p>