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:
+5
-2
@@ -1,10 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://convert.ombrora.com/sitemap.xml
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<SeoHead lang={lang} />
|
||||
<section className="hero">
|
||||
<h1>{t('hero.title')}</h1>
|
||||
<p>{t('hero.subtitle')}</p>
|
||||
|
||||
Reference in New Issue
Block a user