SEO : métadonnées complètes (Open Graph fr_CA, twitter:card, canonical), JSON-LD Organization+WebSite, robots.txt et sitemap.xml, titres/descriptions par page, lang fr-CA
- layout : metadataBase, template de titre « %s | ValoPlex », openGraph (type website, locale fr_CA, siteName), twitter summary_large_image, canonical './', html lang=fr-CA, JSON-LD (Organization + WebSite inLanguage fr-CA) - robots.ts : allow /, disallow /api/, référence au sitemap - sitemap.ts : 7 routes publiques statiques avec priorités - layouts de métadonnées pour les pages client (méthodologie, parc, conditions, confidentialité) - /ka et /stats : titres courts (template) + canonical - /estimation/[id] : generateMetadata dynamique (adresse, municipalité) avec cache() partagé - /estimation/manuelle : metadata + noindex (espace de paramètres infini) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
11 changed files +188 −7
added
app/src/app/conditions/layout.tsx
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +import type { Metadata } from "next"; | |
| 3 | + | |
| 4 | +export const metadata: Metadata = { | |
| 5 | + title: "Conditions d'utilisation", | |
| 6 | + description: | |
| 7 | + "Conditions d'utilisation de ValoPlex : nature du service d'estimation statistique, limites des estimations, sources de données publiques (MAMH) et responsabilités.", | |
| 8 | + alternates: { canonical: "/conditions" }, | |
| 9 | +}; | |
| 10 | + | |
| 11 | +export default function ConditionsLayout({ | |
| 12 | + children, | |
| 13 | +}: { | |
| 14 | + children: React.ReactNode; | |
| 15 | +}) { | |
| 16 | + return children; | |
| 17 | +} | |
added
app/src/app/confidentialite/layout.tsx
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +import type { Metadata } from "next"; | |
| 3 | + | |
| 4 | +export const metadata: Metadata = { | |
| 5 | + title: "Politique de confidentialité", | |
| 6 | + description: | |
| 7 | + "Politique de confidentialité de ValoPlex (Loi 25, Québec) : renseignements recueillis, finalités, durée de conservation, témoins (cookies) et vos droits.", | |
| 8 | + alternates: { canonical: "/confidentialite" }, | |
| 9 | +}; | |
| 10 | + | |
| 11 | +export default function ConfidentialiteLayout({ | |
| 12 | + children, | |
| 13 | +}: { | |
| 14 | + children: React.ReactNode; | |
| 15 | +}) { | |
| 16 | + return children; | |
| 17 | +} | |
modified
app/src/app/estimation/[id]/page.tsx
+26 −1
@@ -1,17 +1,42 @@ | ||
| 1 | 1 | // Auteur : Simon-Pierre Boucher — contact@spboucher.ai |
| 2 | +import { cache } from "react"; | |
| 3 | +import type { Metadata } from "next"; | |
| 2 | 4 | import { notFound } from "next/navigation"; |
| 3 | 5 | import { estimateByUnitId } from "@/lib/estimator"; |
| 4 | 6 | import ResultView from "@/components/ResultView"; |
| 5 | 7 | |
| 6 | 8 | export const dynamic = "force-dynamic"; |
| 7 | 9 | |
| 10 | +// Mémoïsé par requête : partagé entre generateMetadata et le rendu de la page. | |
| 11 | +const getEstimate = cache((id: string) => estimateByUnitId(id)); | |
| 12 | + | |
| 13 | +export async function generateMetadata({ | |
| 14 | + params, | |
| 15 | +}: { | |
| 16 | + params: Promise<{ id: string }>; | |
| 17 | +}): Promise<Metadata> { | |
| 18 | + const { id } = await params; | |
| 19 | + const data = getEstimate(decodeURIComponent(id)); | |
| 20 | + if (!data?.unit) return { title: "Estimation" }; | |
| 21 | + const ou = [data.unit.adresse, data.unit.municipalite] | |
| 22 | + .filter(Boolean) | |
| 23 | + .join(", "); | |
| 24 | + return { | |
| 25 | + title: ou ? `Estimation — ${ou}` : "Estimation", | |
| 26 | + description: ou | |
| 27 | + ? `La valeur estimée de ${ou} : fourchette calibrée, indice de confiance, comparables de plex ajustés porte par porte et calcul montré en entier.` | |
| 28 | + : undefined, | |
| 29 | + alternates: { canonical: `/estimation/${encodeURIComponent(id)}` }, | |
| 30 | + }; | |
| 31 | +} | |
| 32 | + | |
| 8 | 33 | export default async function EstimationPage({ |
| 9 | 34 | params, |
| 10 | 35 | }: { |
| 11 | 36 | params: Promise<{ id: string }>; |
| 12 | 37 | }) { |
| 13 | 38 | const { id } = await params; |
| 14 | − const data = estimateByUnitId(decodeURIComponent(id)); | |
| 39 | + const data = getEstimate(decodeURIComponent(id)); | |
| 15 | 40 | if (!data) notFound(); |
| 16 | 41 | return <ResultView data={data} />; |
| 17 | 42 | } |
modified
app/src/app/estimation/manuelle/page.tsx
+9 −0
@@ -1,10 +1,19 @@ | ||
| 1 | 1 | // Auteur : Simon-Pierre Boucher — contact@spboucher.ai |
| 2 | +import type { Metadata } from "next"; | |
| 2 | 3 | import { notFound } from "next/navigation"; |
| 3 | 4 | import { estimateManual } from "@/lib/estimator"; |
| 4 | 5 | import ResultView from "@/components/ResultView"; |
| 5 | 6 | |
| 6 | 7 | export const dynamic = "force-dynamic"; |
| 7 | 8 | |
| 9 | +export const metadata: Metadata = { | |
| 10 | + title: "Estimation manuelle", | |
| 11 | + description: | |
| 12 | + "Estimez un plex du Québec à partir de ses caractéristiques : municipalité, nombre de portes, superficie et année de construction.", | |
| 13 | + // Espace de paramètres infini → on n'indexe pas ces pages de résultat. | |
| 14 | + robots: { index: false, follow: true }, | |
| 15 | +}; | |
| 16 | + | |
| 8 | 17 | export default async function ManualEstimationPage({ |
| 9 | 18 | searchParams, |
| 10 | 19 | }: { |
modified
app/src/app/ka/page.tsx
+2 −1
@@ -3,9 +3,10 @@ import type { Metadata } from "next"; | ||
| 3 | 3 | import KaHero from "./KaHero"; |
| 4 | 4 | |
| 5 | 5 | export const metadata: Metadata = { |
| 6 | − title: "Ka — l'agent IA de ValoPlex", | |
| 6 | + title: "Ka — l'agent IA plex", | |
| 7 | 7 | description: |
| 8 | 8 | "Ka évalue n'importe quel plex du Québec en conversant : recherche dans le registre, évaluation par porte, pro forma investisseur complet, comparables — en direct.", |
| 9 | + alternates: { canonical: "/ka" }, | |
| 9 | 10 | }; |
| 10 | 11 | |
| 11 | 12 | export default function KaPage() { |
modified
app/src/app/layout.tsx
+45 −4
@@ -18,10 +18,47 @@ const jetbrains = JetBrains_Mono({ | ||
| 18 | 18 | variable: "--font-jetbrains", |
| 19 | 19 | }); |
| 20 | 20 | |
| 21 | +const SITE_URL = "https://www.valoplex.com"; | |
| 22 | +const SITE_TITLE = "ValoPlex — la vraie valeur, sans boîte noire"; | |
| 23 | +const SITE_DESCRIPTION = | |
| 24 | + "Le moteur d'évaluation spécialisé plex du Québec : modèle hédonique avancé + ventes de plex comparables, fourchette calibrée et indice de confiance."; | |
| 25 | + | |
| 21 | 26 | export const metadata: Metadata = { |
| 22 | − title: "ValoPlex — la vraie valeur, sans boîte noire", | |
| 23 | − description: | |
| 24 | − "Le moteur d'évaluation spécialisé plex du Québec : modèle hédonique avancé + ventes de plex comparables, fourchette calibrée et indice de confiance.", | |
| 27 | + metadataBase: new URL(SITE_URL), | |
| 28 | + title: { | |
| 29 | + default: SITE_TITLE, | |
| 30 | + template: "%s | ValoPlex", | |
| 31 | + }, | |
| 32 | + description: SITE_DESCRIPTION, | |
| 33 | + alternates: { canonical: "./" }, | |
| 34 | + openGraph: { | |
| 35 | + type: "website", | |
| 36 | + locale: "fr_CA", | |
| 37 | + url: SITE_URL, | |
| 38 | + siteName: "ValoPlex", | |
| 39 | + title: SITE_TITLE, | |
| 40 | + description: SITE_DESCRIPTION, | |
| 41 | + }, | |
| 42 | + twitter: { card: "summary_large_image" }, | |
| 43 | +}; | |
| 44 | + | |
| 45 | +const JSON_LD = { | |
| 46 | + "@context": "https://schema.org", | |
| 47 | + "@graph": [ | |
| 48 | + { | |
| 49 | + "@type": "Organization", | |
| 50 | + name: "ValoPlex (Groupe KA)", | |
| 51 | + url: SITE_URL, | |
| 52 | + email: "contact@spboucher.ai", | |
| 53 | + }, | |
| 54 | + { | |
| 55 | + "@type": "WebSite", | |
| 56 | + name: "ValoPlex", | |
| 57 | + url: SITE_URL, | |
| 58 | + description: SITE_DESCRIPTION, | |
| 59 | + inLanguage: "fr-CA", | |
| 60 | + }, | |
| 61 | + ], | |
| 25 | 62 | }; |
| 26 | 63 | |
| 27 | 64 | const TICKER = [ |
@@ -37,10 +74,14 @@ const TICKER = [ | ||
| 37 | 74 | |
| 38 | 75 | export default function RootLayout({ children }: LayoutProps<"/">) { |
| 39 | 76 | return ( |
| 40 | − <html lang="fr"> | |
| 77 | + <html lang="fr-CA"> | |
| 41 | 78 | <body |
| 42 | 79 | className={`${inter.variable} ${spaceGrotesk.variable} ${jetbrains.variable}`} |
| 43 | 80 | > |
| 81 | + <script | |
| 82 | + type="application/ld+json" | |
| 83 | + dangerouslySetInnerHTML={{ __html: JSON.stringify(JSON_LD) }} | |
| 84 | + /> | |
| 44 | 85 | <LangProvider> |
| 45 | 86 | {/* fond opaque sur mobile (backdrop-filter casse les enfants fixed sur WebKit) */} |
| 46 | 87 | <header className="sticky top-0 z-[1100] border-b-2 border-ink bg-paper md:bg-[rgba(245,243,238,0.88)] md:backdrop-blur-xl"> |
added
app/src/app/methodologie/layout.tsx
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +import type { Metadata } from "next"; | |
| 3 | + | |
| 4 | +export const metadata: Metadata = { | |
| 5 | + title: "Méthodologie", | |
| 6 | + description: | |
| 7 | + "Comment ValoPlex évalue les plex du Québec : modèle hédonique en ratio prix/rôle, comparables ajustés porte par porte, fourchette P10-P90 calibrée à 80 % et indice de confiance — le calcul montré en entier.", | |
| 8 | + alternates: { canonical: "/methodologie" }, | |
| 9 | +}; | |
| 10 | + | |
| 11 | +export default function MethodologieLayout({ | |
| 12 | + children, | |
| 13 | +}: { | |
| 14 | + children: React.ReactNode; | |
| 15 | +}) { | |
| 16 | + return children; | |
| 17 | +} | |
added
app/src/app/parc/layout.tsx
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +import type { Metadata } from "next"; | |
| 3 | + | |
| 4 | +export const metadata: Metadata = { | |
| 5 | + title: "Parc immobilier", | |
| 6 | + description: | |
| 7 | + "Suivez la valeur de votre parc de plex au Québec : regroupez vos immeubles, obtenez la valeur totale, la valeur par porte, la répartition par municipalité et le rapport investisseur PDF.", | |
| 8 | + alternates: { canonical: "/parc" }, | |
| 9 | +}; | |
| 10 | + | |
| 11 | +export default function ParcLayout({ | |
| 12 | + children, | |
| 13 | +}: { | |
| 14 | + children: React.ReactNode; | |
| 15 | +}) { | |
| 16 | + return children; | |
| 17 | +} | |
added
app/src/app/robots.ts
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +import type { MetadataRoute } from "next"; | |
| 3 | + | |
| 4 | +export default function robots(): MetadataRoute.Robots { | |
| 5 | + return { | |
| 6 | + rules: { | |
| 7 | + userAgent: "*", | |
| 8 | + allow: "/", | |
| 9 | + disallow: ["/api/"], | |
| 10 | + }, | |
| 11 | + sitemap: "https://www.valoplex.com/sitemap.xml", | |
| 12 | + }; | |
| 13 | +} | |
added
app/src/app/sitemap.ts
+23 −0
@@ -0,0 +1,23 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +import type { MetadataRoute } from "next"; | |
| 3 | + | |
| 4 | +const BASE = "https://www.valoplex.com"; | |
| 5 | + | |
| 6 | +export default function sitemap(): MetadataRoute.Sitemap { | |
| 7 | + const lastModified = new Date(); | |
| 8 | + const routes: { path: string; priority: number }[] = [ | |
| 9 | + { path: "/", priority: 1 }, | |
| 10 | + { path: "/ka", priority: 0.9 }, | |
| 11 | + { path: "/methodologie", priority: 0.8 }, | |
| 12 | + { path: "/stats", priority: 0.8 }, | |
| 13 | + { path: "/parc", priority: 0.7 }, | |
| 14 | + { path: "/conditions", priority: 0.3 }, | |
| 15 | + { path: "/confidentialite", priority: 0.3 }, | |
| 16 | + ]; | |
| 17 | + return routes.map(({ path, priority }) => ({ | |
| 18 | + url: `${BASE}${path === "/" ? "" : path}`, | |
| 19 | + lastModified, | |
| 20 | + changeFrequency: "monthly" as const, | |
| 21 | + priority, | |
| 22 | + })); | |
| 23 | +} | |
modified
app/src/app/stats/page.tsx
+2 −1
@@ -3,9 +3,10 @@ import StatsView, { type ProvStats } from "@/components/StatsView"; | ||
| 3 | 3 | import stats from "@/data/stats.json"; |
| 4 | 4 | |
| 5 | 5 | export const metadata = { |
| 6 | − title: "ValoPlex — Statistiques provinciales", | |
| 6 | + title: "Statistiques provinciales", | |
| 7 | 7 | description: |
| 8 | 8 | "La valeur totale de l'immobilier québécois, par municipalité et par type — 3,7 millions de propriétés estimées.", |
| 9 | + alternates: { canonical: "/stats" }, | |
| 9 | 10 | }; |
| 10 | 11 | |
| 11 | 12 | export default function StatsPage() { |
| 12 | 13 | |