SPB Git

spb/valoplex

Public

ValoPlex — moteur d'évaluation spécialisé pour les plex au Québec, petit frère de Vrai-Prix.

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>
Simon-Pierre Boucher committed (Aug 22, 2026) parent 8118a9b Browse files

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 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai 1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2+import { cache } from "react";
3+import type { Metadata } from "next";
2 import { notFound } from "next/navigation"; 4 import { notFound } from "next/navigation";
3 import { estimateByUnitId } from "@/lib/estimator"; 5 import { estimateByUnitId } from "@/lib/estimator";
4 import ResultView from "@/components/ResultView"; 6 import ResultView from "@/components/ResultView";
5 7
6 export const dynamic = "force-dynamic"; 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 export default async function EstimationPage({ 33 export default async function EstimationPage({
9 params, 34 params,
10 }: { 35 }: {
11 params: Promise<{ id: string }>; 36 params: Promise<{ id: string }>;
12 }) { 37 }) {
13 const { id } = await params; 38 const { id } = await params;
14− const data = estimateByUnitId(decodeURIComponent(id)); 39+ const data = getEstimate(decodeURIComponent(id));
15 if (!data) notFound(); 40 if (!data) notFound();
16 return <ResultView data={data} />; 41 return <ResultView data={data} />;
17 } 42 }
modified app/src/app/estimation/manuelle/page.tsx +9 −0
@@ -1,10 +1,19 @@
1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai 1 // Auteur : Simon-Pierre Boucher — contact@spboucher.ai
2+import type { Metadata } from "next";
2 import { notFound } from "next/navigation"; 3 import { notFound } from "next/navigation";
3 import { estimateManual } from "@/lib/estimator"; 4 import { estimateManual } from "@/lib/estimator";
4 import ResultView from "@/components/ResultView"; 5 import ResultView from "@/components/ResultView";
5 6
6 export const dynamic = "force-dynamic"; 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 export default async function ManualEstimationPage({ 17 export default async function ManualEstimationPage({
9 searchParams, 18 searchParams,
10 }: { 19 }: {
modified app/src/app/ka/page.tsx +2 −1
@@ -3,9 +3,10 @@ import type { Metadata } from "next";
3 import KaHero from "./KaHero"; 3 import KaHero from "./KaHero";
4 4
5 export const metadata: Metadata = { 5 export const metadata: Metadata = {
6− title: "Ka — l'agent IA de ValoPlex", 6+ title: "Ka — l'agent IA plex",
7 description: 7 description:
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.", 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 export default function KaPage() { 12 export default function KaPage() {
modified app/src/app/layout.tsx +45 −4
@@ -18,10 +18,47 @@ const jetbrains = JetBrains_Mono({
18 variable: "--font-jetbrains", 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 export const metadata: Metadata = { 26 export const metadata: Metadata = {
22− title: "ValoPlex — la vraie valeur, sans boîte noire", 27+ metadataBase: new URL(SITE_URL),
23− description: 28+ title: {
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.", 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 const TICKER = [ 64 const TICKER = [
@@ -37,10 +74,14 @@ const TICKER = [
37 74
38 export default function RootLayout({ children }: LayoutProps<"/">) { 75 export default function RootLayout({ children }: LayoutProps<"/">) {
39 return ( 76 return (
40− <html lang="fr"> 77+ <html lang="fr-CA">
41 <body 78 <body
42 className={`${inter.variable} ${spaceGrotesk.variable} ${jetbrains.variable}`} 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 <LangProvider> 85 <LangProvider>
45 {/* fond opaque sur mobile (backdrop-filter casse les enfants fixed sur WebKit) */} 86 {/* fond opaque sur mobile (backdrop-filter casse les enfants fixed sur WebKit) */}
46 <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"> 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 import stats from "@/data/stats.json"; 3 import stats from "@/data/stats.json";
4 4
5 export const metadata = { 5 export const metadata = {
6− title: "ValoPlex — Statistiques provinciales", 6+ title: "Statistiques provinciales",
7 description: 7 description:
8 "La valeur totale de l'immobilier québécois, par municipalité et par type — 3,7 millions de propriétés estimées.", 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 export default function StatsPage() { 12 export default function StatsPage() {
12 13
Menu

Theme