diff --git a/TODO-SEO.md b/TODO-SEO.md new file mode 100644 index 0000000..fa3eb6d --- /dev/null +++ b/TODO-SEO.md @@ -0,0 +1,102 @@ +# TODO SEO — myinfomate-landing + +Liste des améliorations SEO à réaliser, classées par impact décroissant. + +--- + +## 1. Multilingue avec hreflang (impact: ÉLEVÉ) ✅ + +Le site a 4 langues (FR/EN/NL/DE) mais tout est servi sur la même URL avec `lang="fr"` figé. Google n'indexe qu'une seule version → on rate les recherches NL et DE. + +- [x] Mettre en place un routing par langue : `/fr`, `/en`, `/nl`, `/de` (ou middleware Next) +- [x] Ajouter `metadata.alternates.languages` dans `layout.tsx` et `[segment]/page.tsx` +- [x] Rendre `` dynamique selon la route +- [x] Adapter le `sitemap.ts` pour générer les URLs de chaque langue +- [x] Vérifier que les balises `` pointent vers la version par défaut + +--- + +## 2. Page d'accueil en Server Component (impact: MOYEN) + +`src/app/page.tsx` est marqué `'use client'` → perte de perfs (FCP/LCP, signaux de ranking). + +- [ ] Convertir `page.tsx` en Server Component +- [ ] Extraire les parties interactives (menu langue, `Reveal`, etc.) dans des sous-composants client +- [ ] Vérifier l'amélioration via Lighthouse avant/après + +--- + +## 3. JSON-LD sur la home (impact: MOYEN) + +Schema.org est présent sur les segments mais pas sur la home. + +- [ ] Ajouter un schéma `Organization` (Unov) avec `logo`, `url`, `sameAs` (LinkedIn, etc.) +- [ ] Ajouter un schéma `WebSite` +- [ ] Ajouter un schéma `SoftwareApplication` pour MyInfoMate (avec `aggregateRating` si avis disponibles) + +--- + +## 4. OG image dédiée (impact: MOYEN) + +Actuellement `/myinfomate-logo.png` est utilisé comme image Open Graph → mauvais rendu sur LinkedIn / WhatsApp / Slack. + +- [ ] Créer une vraie image 1200×630 (titre + visuel produit) +- [ ] Idéalement, utiliser `opengraph-image.tsx` (Next 16) pour génération dynamique +- [ ] Faire une variante par segment (musées, offices tourisme, etc.) + +--- + +## 5. Title et meta description plus stratégiques (impact: MOYEN) + +Le title actuel ("MyInfoMate | La technologie au service de l'expérience visiteur") fait ~63 chars et manque de mots-clés forts. + +- [ ] Réécrire le title de la home avec mots-clés ciblés (ex: "MyInfoMate — Guide visiteur digital pour musées, sites & événements") +- [ ] Réviser titles + descriptions de chaque segment pour viser des requêtes précises (ex: "application musée", "kiosk tactile musée", "guide visiteur tablette") +- [ ] Garder titles < 60 chars et descriptions ~155 chars + +--- + +## 6. Contenu long-tail (impact: ÉLEVÉ à long terme) + +Aujourd'hui : 6 segments + 2 légales = peu de surface SEO. + +- [ ] Créer une section `/cas-clients/[slug]` (un par client référent) +- [ ] Créer une section `/blog/[slug]` ou `/ressources/[slug]` avec 3-5 articles fondateurs : + - [ ] "Audioguide vs application visiteur : que choisir en 2026 ?" + - [ ] "RGPD et tracking visiteur dans un musée" + - [ ] "ROI d'une application visiteur pour un site culturel" + - [ ] "Comment digitaliser un parcours de visite" + - [ ] "Kiosk tactile vs BYOD : avantages et limites" +- [ ] Créer des pages comparatives (très efficace B2B) : + - [ ] `/comparatif/myinfomate-vs-smartify` + - [ ] `/comparatif/myinfomate-vs-stqry` + - [ ] `/comparatif/myinfomate-vs-orpheo` + +--- + +## 7. Détails techniques (impact: FAIBLE à MOYEN) + +- [ ] Vérifier que toutes les `` ont un `alt` descriptif (pas juste "logo") +- [ ] S'assurer qu'il y a un H1 unique et explicite par page +- [ ] Ajouter `priority` sur l'image hero (amélioration LCP) +- [ ] Passer la font Material Symbols de `display=block` à `display=swap` (évite le blocage de rendu) +- [ ] Vérifier le score PageSpeed/Lighthouse (cible: > 90 sur mobile) +- [ ] Soumettre le sitemap dans Google Search Console et Bing Webmaster Tools +- [ ] Mettre en place un suivi des positions (Search Console suffit pour démarrer) + +--- + +## 8. Backlinks / off-site (impact: ÉLEVÉ, hors code) + +Le SEO on-page sera bientôt solide ; le levier suivant est le netlinking. + +- [ ] Référencement dans les annuaires culture/tech belges : + - [ ] Digital Wallonia + - [ ] AWEX + - [ ] hub.brussels +- [ ] Citations dans la presse spécialisée musées : + - [ ] CLIC France + - [ ] ICOM + - [ ] Museumnext +- [ ] Demander un lien retour depuis les sites des clients référents +- [ ] Profil LinkedIn d'entreprise actif (publications régulières → trafic indirect) diff --git a/next.config.ts b/next.config.ts index fdfc499..2c7f365 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,8 +1,12 @@ import type { NextConfig } from "next"; +import path from "node:path"; const nextConfig: NextConfig = { output: "standalone", reactCompiler: true, + turbopack: { + root: path.resolve(__dirname), + }, }; export default nextConfig; diff --git a/src/app/page.tsx b/src/app/[lang]/HomeClient.tsx similarity index 89% rename from src/app/page.tsx rename to src/app/[lang]/HomeClient.tsx index a55a02d..2f47291 100644 --- a/src/app/page.tsx +++ b/src/app/[lang]/HomeClient.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import Image from 'next/image'; +import { useRouter } from 'next/navigation'; import { resolveImage } from '@/data/stitch-images'; import translations, { Language } from '@/data/translations'; @@ -14,6 +15,7 @@ const MODULE_META = [ { id: 'offline', icon: 'download_for_offline', theme: 'rose' }, { id: 'web', icon: 'public', theme: 'rose' }, { id: 'stats', icon: 'monitoring', theme: 'rose' }, + { id: 'escapegame', icon: 'explore', theme: 'rose' }, ]; const LANGUAGES: { code: Language; label: string; name: string }[] = [ @@ -53,7 +55,11 @@ function Reveal({ children, delay = 0, className = '' }: { children: React.React ); } -export default function Home() { +export default function Home({ lang }: { lang: Language }) { + const router = useRouter(); + const setLang = (next: Language) => { + if (next !== lang) router.push(`/${next}`); + }; const [formData, setFormData] = useState({ firstName: '', lastName: '', @@ -64,7 +70,6 @@ export default function Home() { const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); const [activeModule, setActiveModule] = useState(0); const [isMenuOpen, setIsMenuOpen] = useState(false); - const [lang, setLang] = useState('fr'); const [isLangOpen, setIsLangOpen] = useState(false); const langDropdownRef = useRef(null); const [navVisible, setNavVisible] = useState(true); @@ -100,11 +105,6 @@ export default function Home() { return () => { document.body.style.overflow = 'auto'; }; }, [isMenuOpen]); - // Update html lang attribute - useEffect(() => { - document.documentElement.lang = lang; - }, [lang]); - // Close lang dropdown on outside click useEffect(() => { const handler = (e: MouseEvent) => { @@ -817,58 +817,82 @@ export default function Home() {
-
+
museum

{t.audience.item1Title}

-

{t.audience.item1Desc}

-
+

{t.audience.item1Desc}

+ + {t.audience.item1Cta} + arrow_forward + +
-
+
travel_explore

{t.audience.item2Title}

-

{t.audience.item2Desc}

-
+

{t.audience.item2Desc}

+ + {t.audience.item2Cta} + arrow_forward + +
-
+
park

{t.audience.item3Title}

-

{t.audience.item3Desc}

-
+

{t.audience.item3Desc}

+ + {t.audience.item3Cta} + arrow_forward + +
-
+
hotel

{t.audience.item4Title}

-

{t.audience.item4Desc}

-
+

{t.audience.item4Desc}

+ + {t.audience.item4Cta} + arrow_forward + +
-
+
festival

{t.audience.item5Title}

-

{t.audience.item5Desc}

-
+

{t.audience.item5Desc}

+ + {t.audience.item5Cta} + arrow_forward + +
-
+
school

{t.audience.item6Title}

-

{t.audience.item6Desc}

-
+

{t.audience.item6Desc}

+ + {t.audience.item6Cta} + arrow_forward + +
@@ -885,27 +909,24 @@ export default function Home() { -
+
- {/* Starter */} + {/* Essentiel */} -
+
+
+ {t.pricing.comingSoon} +
-

Starter

+

Essentiel

- {t.pricing.startingFrom} - €69 + €39 {t.pricing.perMonth}
-

{t.pricing.htva}

+

{t.pricing.htva} · {t.pricing.noCommitment}

    - {[ - t.pricing.features.mobileApp, - t.pricing.features.kioskApp, - t.pricing.features.backoffice, - t.pricing.features.sections, - ].map((f) => ( + {[t.pricing.features.webAndKiosk, t.pricing.features.backoffice, t.pricing.features.sections].map((f) => (
  • check_circle {f} @@ -913,14 +934,13 @@ export default function Home() { ))}
  • check_circle - 2 GB {t.pricing.features.storage} + 1 GB {t.pricing.features.storage}
  • - {[ - t.pricing.features.autoTranslation, - t.pricing.features.pushNotif, - t.pricing.features.stats, - t.pricing.features.ai, - ].map((f) => ( +
  • + check_circle + {t.pricing.features.stats} — {t.pricing.features.statsBasicLabel} +
  • + {[t.pricing.features.nativeApp, t.pricing.features.offlineBeacons, t.pricing.features.pushNotif, t.pricing.features.ai, t.pricing.features.autoTranslation].map((f) => (
  • cancel {f} @@ -933,28 +953,23 @@ export default function Home() {
- {/* Standard */} - + {/* Pro */} +
{t.pricing.recommended}
-

Standard

+

Pro

- {t.pricing.startingFrom} €99 {t.pricing.perMonth}

{t.pricing.htva}

+

{t.pricing.setupFeeNote}

    - {[ - t.pricing.features.mobileApp, - t.pricing.features.kioskApp, - t.pricing.features.backoffice, - t.pricing.features.sections, - ].map((f) => ( + {[t.pricing.features.webAndKiosk, t.pricing.features.nativeApp, t.pricing.features.backoffice, t.pricing.features.sections].map((f) => (
  • check_circle {f} @@ -962,12 +977,9 @@ export default function Home() { ))}
  • check_circle - 10 GB {t.pricing.features.storage} + 15 GB {t.pricing.features.storage}
  • - {[ - t.pricing.features.autoTranslation, - t.pricing.features.pushNotif, - ].map((f) => ( + {[t.pricing.features.offlineBeacons, t.pricing.features.pushNotif].map((f) => (
  • check_circle {f} @@ -975,12 +987,14 @@ export default function Home() { ))}
  • check_circle - {t.pricing.features.stats} ({t.pricing.features.statsBasic}) -
  • -
  • - check_circle - {t.pricing.features.ai} — 500 {t.pricing.features.reqPerMonth} + {t.pricing.features.stats} — {t.pricing.features.statsBasicLabel}
  • + {[t.pricing.features.statsAdvancedFeature, t.pricing.features.ai, t.pricing.features.autoTranslation].map((f) => ( +
  • + cancel + {f} +
  • + ))}
- {/* Premium */} - -
+ {/* Bundle */} + +
-

Premium

+

Bundle

- {t.pricing.startingFrom} - €199 + €179 {t.pricing.perMonth}

{t.pricing.htva}

+

{t.pricing.setupFeeNote}

    - {[ - t.pricing.features.mobileApp, - t.pricing.features.kioskApp, - t.pricing.features.backoffice, - t.pricing.features.sections, - ].map((f) => ( -
  • - check_circle - {f} -
  • - ))} -
  • - check_circle - 50 GB {t.pricing.features.storage} -
  • - {[ - t.pricing.features.autoTranslation, - t.pricing.features.pushNotif, - ].map((f) => ( -
  • - check_circle - {f} -
  • - ))} -
  • - check_circle - {t.pricing.features.stats} ({t.pricing.features.statsAdvanced}) -
  • -
  • - check_circle - {t.pricing.features.ai} — 2 000 {t.pricing.features.reqPerMonth} -
  • -
- -
-
- - {/* Entreprise */} - -
-
-

Entreprise

-

{t.pricing.enterprisePrice}

-
-

{t.pricing.enterpriseDesc}

-
    - {[ - t.pricing.features.mobileApp, - t.pricing.features.kioskApp, - t.pricing.features.autoTranslation, - t.pricing.features.pushNotif, - ].map((f) => ( + {[t.pricing.features.webAndKiosk, t.pricing.features.nativeApp, t.pricing.features.backoffice, t.pricing.features.sections].map((f) => (
  • check_circle {f} @@ -1062,18 +1023,83 @@ export default function Home() { ))}
  • check_circle - {t.pricing.features.storage} {t.pricing.features.custom} + 50 GB {t.pricing.features.storage} +
  • + {[t.pricing.features.offlineBeacons, t.pricing.features.pushNotif].map((f) => ( +
  • + check_circle + {f} +
  • + ))} +
  • + check_circle + {t.pricing.features.statsAdvancedFeature} +
  • +
  • + check_circle + + {t.pricing.features.ai} — 2 000 {t.pricing.features.reqPerMonth} + {t.pricing.features.aiNeedMore} → Enterprise +
  • check_circle - {t.pricing.features.stats} {t.pricing.features.custom} -
  • -
  • - check_circle - {t.pricing.features.ai} {t.pricing.features.custom} + {t.pricing.features.autoTranslation}
- +
+
+ + {/* Enterprise */} + +
+
+

Enterprise

+
+ {t.pricing.enterprisePrice} +
+

{t.pricing.enterpriseDesc}

+
+
    + {[ + t.pricing.features.webAndKiosk, + t.pricing.features.nativeApp, + t.pricing.features.backoffice, + t.pricing.features.sections, + t.pricing.features.multiSite, + ].map((f) => ( +
  • + check_circle + {f} +
  • + ))} +
  • + check_circle + {t.pricing.features.customStorage} +
  • +
  • + check_circle + {t.pricing.features.customAiQuota} +
  • + {[ + t.pricing.features.statsAdvancedFeature, + t.pricing.features.offlineBeacons, + t.pricing.features.pushNotif, + t.pricing.features.autoTranslation, + t.pricing.features.slaEnhanced, + t.pricing.features.dedicatedSupport, + t.pricing.features.customDev, + ].map((f) => ( +
  • + check_circle + {f} +
  • + ))} +
+
diff --git a/src/app/[lang]/[segment]/SegmentPageClient.tsx b/src/app/[lang]/[segment]/SegmentPageClient.tsx new file mode 100644 index 0000000..19d3235 --- /dev/null +++ b/src/app/[lang]/[segment]/SegmentPageClient.tsx @@ -0,0 +1,739 @@ +'use client'; + +import React, { useState, useEffect, useRef } from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { resolveImage } from '@/data/stitch-images'; +import translations, { Language } from '@/data/translations'; +import type { Segment } from '@/data/segments'; + +const LANGUAGES: { code: Language; label: string; name: string }[] = [ + { code: 'fr', label: 'FR', name: 'Français' }, + { code: 'en', label: 'EN', name: 'English' }, + { code: 'nl', label: 'NL', name: 'Nederlands' }, + { code: 'de', label: 'DE', name: 'Deutsch' }, +]; + +function Reveal({ + children, + delay = 0, + className = '', +}: { + children: React.ReactNode; + delay?: number; + className?: string; +}) { + const ref = React.useRef(null); + const [visible, setVisible] = React.useState(false); + + React.useEffect(() => { + const el = ref.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) setVisible(true); + }, + { threshold: 0.12 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + return ( +
+ {children} +
+ ); +} + +export default function SegmentPageClient({ data, lang }: { data: Segment; lang: Language }) { + const router = useRouter(); + const setLang = (next: Language) => { + if (next !== lang) router.push(`/${next}/${data.slug}`); + }; + const [isLangOpen, setIsLangOpen] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [navVisible, setNavVisible] = useState(true); + const [openFaq, setOpenFaq] = useState(null); + const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '', message: '' }); + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + const langDropdownRef = useRef(null); + const lastScrollY = useRef(0); + + const t = translations[lang]; + const s = data.translations[lang as keyof typeof data.translations]; + + useEffect(() => { + const handleScroll = () => { + const currentY = window.scrollY; + setNavVisible(currentY < lastScrollY.current || currentY < 10); + lastScrollY.current = currentY; + }; + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + useEffect(() => { + document.body.style.overflow = isMenuOpen ? 'hidden' : 'auto'; + return () => { document.body.style.overflow = 'auto'; }; + }, [isMenuOpen]); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (langDropdownRef.current && !langDropdownRef.current.contains(e.target as Node)) { + setIsLangOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + const scrollToContact = (e: React.MouseEvent) => { + e.preventDefault(); + document.getElementById('contact')?.scrollIntoView({ behavior: 'smooth' }); + }; + + const scrollToPricing = (e: React.MouseEvent) => { + e.preventDefault(); + document.getElementById('pricing')?.scrollIntoView({ behavior: 'smooth' }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setStatus('loading'); + try { + const response = await fetch('https://formspree.io/f/xbdaajlo', { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify(formData), + }); + if (response.ok) { + setStatus('success'); + setFormData({ firstName: '', lastName: '', email: '', message: '' }); + } else { + setStatus('error'); + } + } catch { + setStatus('error'); + } + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + return ( +
+ + {/* Navbar */} +
+
+ + MyInfoMate Logo + MyInfoMate + + +
+ + {t.nav.login} + + +
+ + {isLangOpen && ( +
+ {LANGUAGES.map((l) => ( + + ))} +
+ )} +
+ +
+
+
+ + {/* Mobile Menu */} +
setIsMenuOpen(false)} + /> +
+
+
+ +
+ +
+ +
+
+
+ +
+ + {/* Breadcrumb */} +
+ + arrow_back + {s.nav.backLabel} + +
+ + {/* Hero */} +
+
+ + + museum + {s.hero.badge} + +

+ {s.hero.title} +

+

+ {s.hero.subtitle} +

+
+ + +
+
+
+
+ + {/* Pain Points */} +
+
+ +

{s.painPoints.label}

+

{s.painPoints.title}

+
+
+ {s.painPoints.items.map((item, i) => ( + +
+
+ {item.icon} +
+

{item.title}

+

{item.desc}

+
+
+ ))} +
+
+
+ + {/* Features */} +
+
+ +

{s.features.label}

+

{s.features.title}

+

{s.features.desc}

+
+
+ {s.features.items.map((item, i) => ( + +
+
+ {item.icon} +
+

{item.title}

+

{item.desc}

+
+

+ {s.features.valueLabel} + {item.value} +

+
+
+
+ ))} +
+
+
+ + {/* Comparison */} +
+
+ +

{s.comparison.label}

+

{s.comparison.title}

+
+
+ {s.comparison.items.map((item, i) => ( + +
+
+
+ compare +
+
+

vs

+

{item.competitor}

+
+
+
    + {item.advantages.map((adv, j) => ( +
  • + check_circle + {adv} +
  • + ))} +
+
+
+ ))} +
+
+
+ + {/* Pricing */} +
+
+ +

{t.pricing.sectionLabel}

+

{t.pricing.sectionTitle}

+

{t.pricing.sectionDesc}

+
+
+ + {/* Essentiel */} + +
+
+ {t.pricing.comingSoon} +
+
+

Essentiel

+
+ €39 + {t.pricing.perMonth} +
+

{t.pricing.htva} · {t.pricing.noCommitment}

+
+
    + {[t.pricing.features.webAndKiosk, t.pricing.features.backoffice, t.pricing.features.sections].map((f) => ( +
  • + check_circle + {f} +
  • + ))} +
  • + check_circle + 1 GB {t.pricing.features.storage} +
  • +
  • + check_circle + {t.pricing.features.stats} — {t.pricing.features.statsBasicLabel} +
  • + {[t.pricing.features.nativeApp, t.pricing.features.offlineBeacons, t.pricing.features.pushNotif, t.pricing.features.ai, t.pricing.features.autoTranslation].map((f) => ( +
  • + cancel + {f} +
  • + ))} +
+ +
+
+ + {/* Pro */} + +
+
+ {t.pricing.recommended} +
+
+

Pro

+
+ €99 + {t.pricing.perMonth} +
+

{t.pricing.htva}

+

{t.pricing.setupFeeNote}

+
+
    + {[t.pricing.features.webAndKiosk, t.pricing.features.nativeApp, t.pricing.features.backoffice, t.pricing.features.sections].map((f) => ( +
  • + check_circle + {f} +
  • + ))} +
  • + check_circle + 15 GB {t.pricing.features.storage} +
  • + {[t.pricing.features.offlineBeacons, t.pricing.features.pushNotif].map((f) => ( +
  • + check_circle + {f} +
  • + ))} +
  • + check_circle + {t.pricing.features.stats} — {t.pricing.features.statsBasicLabel} +
  • + {[t.pricing.features.statsAdvancedFeature, t.pricing.features.ai, t.pricing.features.autoTranslation].map((f) => ( +
  • + cancel + {f} +
  • + ))} +
+ +
+
+ + {/* Bundle */} + +
+
+

Bundle

+
+ €179 + {t.pricing.perMonth} +
+

{t.pricing.htva}

+

{t.pricing.setupFeeNote}

+
+
    + {[t.pricing.features.webAndKiosk, t.pricing.features.nativeApp, t.pricing.features.backoffice, t.pricing.features.sections].map((f) => ( +
  • + check_circle + {f} +
  • + ))} +
  • + check_circle + 50 GB {t.pricing.features.storage} +
  • + {[t.pricing.features.offlineBeacons, t.pricing.features.pushNotif].map((f) => ( +
  • + check_circle + {f} +
  • + ))} +
  • + check_circle + {t.pricing.features.statsAdvancedFeature} +
  • +
  • + check_circle + {t.pricing.features.ai} — 2 000 {t.pricing.features.reqPerMonth} +
  • +
  • + check_circle + {t.pricing.features.autoTranslation} +
  • +
+ +
+
+ +
+
+
+ + {/* FAQ */} +
+
+ +

{s.faq.label}

+

{s.faq.title}

+
+
+ {s.faq.items.map((item, i) => ( + +
+ +
+

{item.answer}

+
+
+
+ ))} +
+
+
+ + {/* CTA */} +
+
+ +
+
+
+
+
+

{s.cta.title}

+

{s.cta.subtitle}

+
+ + + {s.cta.button2} + +
+
+
+
+
+ + {/* Contact */} +
+
+
+
+
+
+

{t.contact.sectionLabel}

+

{t.contact.title}

+

{t.contact.subtitle}

+
+ {status === 'success' ? ( +
+
+ task_alt +
+

{t.contact.successTitle}

+

{t.contact.successDesc}

+ +
+ ) : ( +
+ {status === 'error' && ( +
+ error + {t.contact.errorMsg} +
+ )} +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ +
+ )} +
+
+
+ +
+ + {/* Footer */} + + +
+ ); +} diff --git a/src/app/[lang]/[segment]/page.tsx b/src/app/[lang]/[segment]/page.tsx new file mode 100644 index 0000000..dbdceb4 --- /dev/null +++ b/src/app/[lang]/[segment]/page.tsx @@ -0,0 +1,112 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { getSegmentData, getAllSegmentSlugs } from '@/data/segments'; +import { LOCALES, LOCALE_HTML_LANG, LOCALE_OG, DEFAULT_LOCALE, isLocale } from '@/i18n'; +import SegmentPageClient from './SegmentPageClient'; + +const SITE_URL = 'https://myinfomate.be'; + +export function generateStaticParams() { + const slugs = getAllSegmentSlugs(); + return LOCALES.flatMap((lang) => + slugs.map((segment) => ({ lang, segment })) + ); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ lang: string; segment: string }>; +}): Promise { + const { lang, segment } = await params; + if (!isLocale(lang)) return {}; + const data = getSegmentData(segment); + if (!data) return {}; + const m = data.meta[lang]; + const languages = Object.fromEntries( + LOCALES.map((l) => [LOCALE_HTML_LANG[l], `/${l}/${segment}`]) + ); + return { + title: m.title, + description: m.description, + openGraph: { + title: m.title, + description: m.description, + url: `${SITE_URL}/${lang}/${segment}`, + siteName: 'MyInfoMate', + locale: LOCALE_OG[lang], + alternateLocale: LOCALES.filter((l) => l !== lang).map((l) => LOCALE_OG[l]), + type: 'website', + images: [{ url: '/myinfomate-logo.png' }], + }, + twitter: { + card: 'summary_large_image', + title: m.title, + description: m.description, + }, + alternates: { + canonical: `/${lang}/${segment}`, + languages: { ...languages, 'x-default': `/${DEFAULT_LOCALE}/${segment}` }, + }, + }; +} + +export default async function SegmentPage({ + params, +}: { + params: Promise<{ lang: string; segment: string }>; +}) { + const { lang, segment } = await params; + if (!isLocale(lang)) notFound(); + const data = getSegmentData(segment); + if (!data) notFound(); + + const faqItems = data.translations[lang].faq.items; + const faqSchema = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: faqItems.map((item) => ({ + '@type': 'Question', + name: item.question, + acceptedAnswer: { + '@type': 'Answer', + text: item.answer, + }, + })), + }; + + const softwareSchema = { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: 'MyInfoMate', + applicationCategory: 'BusinessApplication', + operatingSystem: 'iOS, Android, Web', + offers: { + '@type': 'Offer', + price: '39', + priceCurrency: 'EUR', + priceSpecification: { + '@type': 'UnitPriceSpecification', + price: '39', + priceCurrency: 'EUR', + unitText: 'MONTH', + }, + }, + description: data.meta[lang].description, + url: `${SITE_URL}/${lang}/${segment}`, + }; + + return ( + <> +