SPB Git forge

spb/valoplex

Public

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

11commits 1branches 0releases
2.4 MBsize
maindefault branch
20 days agolast push
TypeScript 91.9% Python 6% CSS 2.1%

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 1 mo ago (Aug 22, 2026) parent 8118a9b

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