feat(carbets): public search + carbet detail page (SSR/SEO)
Implémente SYS-5 : la marketplace publique pour découvrir les carbets
fluviaux publiés par les hôtes.
- /carbets : page de recherche server-side avec filtres GET
(fleuve, dates de séjour, capacité min.), grille de résultats
avec photo de couverture, fleuve, capacité, durée pirogue
- /carbets/[slug] : fiche carbet SSR
- generateMetadata (title/description + OpenGraph/Twitter cards)
- galerie médias (photo couverture + vignettes vidéo/photo)
- description, équipements (catalogue), accès, coords GPS,
capacité, prénom de l'hôte
- robots.ts + sitemap.xml (incluant les carbets publiés)
- metadataBase / title.template au niveau du root layout, OG par
défaut Karbé
- Lien "Découvrir les carbets" sur la home
- Helpers partagés : lib/carbet-search.ts (parse filters + query),
lib/carbet-public.ts (fetch SSR mémoïsé via React cache),
lib/format.ts (durée pirogue, troncature, coords)
- Nouvelle variable d'env NEXT_PUBLIC_SITE_URL (canonical/OG/sitemap)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
3567eb975b
commit
c2df6722f2
13 changed files with 827 additions and 1 deletions
171
src/app/carbets/[slug]/page.tsx
Normal file
171
src/app/carbets/[slug]/page.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getPublicCarbet } from "@/lib/carbet-public";
|
||||
import {
|
||||
formatCoordinate,
|
||||
formatPirogueDuration,
|
||||
truncate,
|
||||
} from "@/lib/format";
|
||||
import { MediaType } from "@/generated/prisma/enums";
|
||||
|
||||
import { CarbetGallery } from "../_components/carbet-gallery";
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ slug: string }>;
|
||||
};
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: PageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const carbet = await getPublicCarbet(slug);
|
||||
|
||||
if (!carbet) {
|
||||
return {
|
||||
title: "Carbet introuvable",
|
||||
robots: { index: false, follow: false },
|
||||
};
|
||||
}
|
||||
|
||||
const description = truncate(carbet.description, 200);
|
||||
const coverPhoto = carbet.media.find((m) => m.type === MediaType.PHOTO);
|
||||
const ogImages = coverPhoto
|
||||
? [{ url: coverPhoto.url, alt: `Photo de ${carbet.title}` }]
|
||||
: undefined;
|
||||
const canonical = `/carbets/${carbet.slug}`;
|
||||
|
||||
return {
|
||||
title: `${carbet.title} — Carbet sur ${carbet.river}`,
|
||||
description,
|
||||
alternates: { canonical },
|
||||
openGraph: {
|
||||
type: "website",
|
||||
title: `${carbet.title} — Carbet sur le fleuve ${carbet.river}`,
|
||||
description,
|
||||
url: canonical,
|
||||
siteName: "Karbé",
|
||||
locale: "fr_FR",
|
||||
images: ogImages,
|
||||
},
|
||||
twitter: {
|
||||
card: ogImages ? "summary_large_image" : "summary",
|
||||
title: carbet.title,
|
||||
description,
|
||||
images: ogImages?.map((img) => img.url),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PublicCarbetPage({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
const carbet = await getPublicCarbet(slug);
|
||||
|
||||
if (!carbet) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl flex-1 px-6 py-10">
|
||||
<Link
|
||||
href="/carbets"
|
||||
className="text-sm text-zinc-600 hover:text-zinc-900"
|
||||
>
|
||||
← Tous les carbets
|
||||
</Link>
|
||||
|
||||
<header className="mt-3">
|
||||
<p className="text-sm font-medium uppercase tracking-wide text-emerald-700">
|
||||
Fleuve {carbet.river}
|
||||
</p>
|
||||
<h1 className="mt-1 text-3xl font-semibold text-zinc-900 sm:text-4xl">
|
||||
{carbet.title}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-zinc-600">
|
||||
Accueil par {carbet.ownerFirstName} · {carbet.capacity} voyageur
|
||||
{carbet.capacity > 1 ? "s" : ""} · Pirogue{" "}
|
||||
{formatPirogueDuration(carbet.pirogueDurationMin)} depuis{" "}
|
||||
{carbet.embarkPoint}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="mt-6">
|
||||
<CarbetGallery title={carbet.title} media={carbet.media} />
|
||||
</section>
|
||||
|
||||
<div className="mt-10 grid gap-10 lg:grid-cols-3">
|
||||
<div className="lg:col-span-2">
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-zinc-900">
|
||||
À propos de ce carbet
|
||||
</h2>
|
||||
<p className="mt-3 whitespace-pre-line text-zinc-700">
|
||||
{carbet.description}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{carbet.amenities.length > 0 ? (
|
||||
<section className="mt-10">
|
||||
<h2 className="text-xl font-semibold text-zinc-900">
|
||||
Équipements
|
||||
</h2>
|
||||
<ul className="mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{carbet.amenities.map((amenity) => (
|
||||
<li
|
||||
key={amenity.key}
|
||||
className="flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700"
|
||||
>
|
||||
<span aria-hidden className="text-emerald-600">
|
||||
●
|
||||
</span>
|
||||
{amenity.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<aside className="space-y-4 rounded-lg border border-zinc-200 bg-white p-5">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold text-zinc-900">
|
||||
Accès au carbet
|
||||
</h2>
|
||||
<dl className="mt-3 space-y-2 text-sm text-zinc-700">
|
||||
<div>
|
||||
<dt className="font-medium text-zinc-500">
|
||||
Point d'embarquement
|
||||
</dt>
|
||||
<dd>{carbet.embarkPoint}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-zinc-500">Trajet pirogue</dt>
|
||||
<dd>{formatPirogueDuration(carbet.pirogueDurationMin)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-zinc-500">Coordonnées GPS</dt>
|
||||
<dd>
|
||||
{formatCoordinate(carbet.latitude)},{" "}
|
||||
{formatCoordinate(carbet.longitude)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-medium text-zinc-500">Capacité</dt>
|
||||
<dd>
|
||||
{carbet.capacity} voyageur
|
||||
{carbet.capacity > 1 ? "s" : ""}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<p className="rounded-md bg-emerald-50 px-3 py-2 text-xs text-emerald-800">
|
||||
La réservation en ligne arrive bientôt. En attendant, contactez
|
||||
l'équipe Karbé pour organiser votre séjour.
|
||||
</p>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
47
src/app/carbets/_components/carbet-card.tsx
Normal file
47
src/app/carbets/_components/carbet-card.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import type { CarbetSearchResult } from "@/lib/carbet-search";
|
||||
import { formatPirogueDuration, truncate } from "@/lib/format";
|
||||
|
||||
export function CarbetCard({ carbet }: { carbet: CarbetSearchResult }) {
|
||||
const href = `/carbets/${carbet.slug}`;
|
||||
return (
|
||||
<article className="group flex flex-col overflow-hidden rounded-lg border border-zinc-200 bg-white shadow-sm transition hover:border-emerald-300 hover:shadow-md">
|
||||
<Link href={href} className="relative block aspect-[4/3] bg-zinc-100">
|
||||
{carbet.coverUrl ? (
|
||||
// Use a plain <img> here — uploaded media URLs come from MinIO/S3 and
|
||||
// don't go through next/image's optimizer in this environment.
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={carbet.coverUrl}
|
||||
alt={`Photo de ${carbet.title}`}
|
||||
loading="lazy"
|
||||
className="h-full w-full object-cover transition group-hover:scale-[1.02]"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-zinc-400">
|
||||
Pas de photo
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
<div className="flex flex-1 flex-col p-4">
|
||||
<h3 className="text-lg font-semibold text-zinc-900">
|
||||
<Link href={href} className="hover:text-emerald-700">
|
||||
{carbet.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-zinc-600">
|
||||
Fleuve {carbet.river} · {carbet.capacity} voyageur
|
||||
{carbet.capacity > 1 ? "s" : ""}
|
||||
</p>
|
||||
<p className="mt-2 line-clamp-3 text-sm text-zinc-600">
|
||||
{truncate(carbet.description, 180)}
|
||||
</p>
|
||||
<p className="mt-3 text-xs text-zinc-500">
|
||||
Pirogue {formatPirogueDuration(carbet.pirogueDurationMin)} depuis{" "}
|
||||
{carbet.embarkPoint}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
73
src/app/carbets/_components/carbet-gallery.tsx
Normal file
73
src/app/carbets/_components/carbet-gallery.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import type { PublicCarbetMedia } from "@/lib/carbet-public";
|
||||
import { MediaType } from "@/generated/prisma/enums";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
media: PublicCarbetMedia[];
|
||||
};
|
||||
|
||||
// SSR-friendly gallery: shows a cover (photo or video) plus a strip of
|
||||
// secondary media. No client component — all native HTML controls.
|
||||
export function CarbetGallery({ title, media }: Props) {
|
||||
if (media.length === 0) {
|
||||
return (
|
||||
<div className="flex aspect-[16/9] w-full items-center justify-center rounded-lg bg-zinc-100 text-sm text-zinc-400">
|
||||
Pas encore de média pour ce carbet.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [cover, ...rest] = media;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<figure className="overflow-hidden rounded-lg bg-zinc-100">
|
||||
{cover.type === MediaType.VIDEO ? (
|
||||
<video
|
||||
src={cover.url}
|
||||
controls
|
||||
playsInline
|
||||
preload="metadata"
|
||||
className="aspect-[16/9] w-full bg-black object-contain"
|
||||
/>
|
||||
) : (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={cover.url}
|
||||
alt={`Photo principale de ${title}`}
|
||||
className="aspect-[16/9] w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</figure>
|
||||
|
||||
{rest.length > 0 ? (
|
||||
<ul className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
{rest.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="overflow-hidden rounded-md bg-zinc-100"
|
||||
>
|
||||
{item.type === MediaType.VIDEO ? (
|
||||
<video
|
||||
src={item.url}
|
||||
preload="metadata"
|
||||
controls
|
||||
playsInline
|
||||
className="aspect-square w-full bg-black object-contain"
|
||||
/>
|
||||
) : (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={item.url}
|
||||
alt={`Média de ${title}`}
|
||||
loading="lazy"
|
||||
className="aspect-square w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
90
src/app/carbets/_components/search-filters.tsx
Normal file
90
src/app/carbets/_components/search-filters.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import type { CarbetSearchFilters } from "@/lib/carbet-search";
|
||||
|
||||
type SearchFiltersProps = {
|
||||
filters: CarbetSearchFilters;
|
||||
rivers: string[];
|
||||
};
|
||||
|
||||
function toDateInput(date: Date | undefined): string {
|
||||
if (!date) return "";
|
||||
// The Date was built from a YYYY-MM-DD UTC string, so toISOString() gives us
|
||||
// back the same calendar day regardless of the server timezone.
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function SearchFilters({ filters, rivers }: SearchFiltersProps) {
|
||||
return (
|
||||
<form
|
||||
method="get"
|
||||
action="/carbets"
|
||||
className="grid gap-4 rounded-lg border border-zinc-200 bg-white p-5 sm:grid-cols-2 lg:grid-cols-5"
|
||||
>
|
||||
<label className="flex flex-col gap-1 text-sm lg:col-span-2">
|
||||
<span className="font-medium text-zinc-700">Fleuve ou rivière</span>
|
||||
<input
|
||||
type="text"
|
||||
name="river"
|
||||
defaultValue={filters.river ?? ""}
|
||||
placeholder="Maroni, Approuague, Oyapock…"
|
||||
list="known-rivers"
|
||||
className="rounded-md border border-zinc-300 px-3 py-2 text-zinc-900 focus:border-emerald-500 focus:outline-none"
|
||||
/>
|
||||
{rivers.length > 0 ? (
|
||||
<datalist id="known-rivers">
|
||||
{rivers.map((river) => (
|
||||
<option key={river} value={river} />
|
||||
))}
|
||||
</datalist>
|
||||
) : null}
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-zinc-700">Arrivée</span>
|
||||
<input
|
||||
type="date"
|
||||
name="startDate"
|
||||
defaultValue={toDateInput(filters.startDate)}
|
||||
className="rounded-md border border-zinc-300 px-3 py-2 text-zinc-900 focus:border-emerald-500 focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-zinc-700">Départ</span>
|
||||
<input
|
||||
type="date"
|
||||
name="endDate"
|
||||
defaultValue={toDateInput(filters.endDate)}
|
||||
className="rounded-md border border-zinc-300 px-3 py-2 text-zinc-900 focus:border-emerald-500 focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
<span className="font-medium text-zinc-700">Voyageurs</span>
|
||||
<input
|
||||
type="number"
|
||||
name="capacity"
|
||||
min={1}
|
||||
max={100}
|
||||
defaultValue={filters.capacity ?? ""}
|
||||
placeholder="Nombre min."
|
||||
className="rounded-md border border-zinc-300 px-3 py-2 text-zinc-900 focus:border-emerald-500 focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex items-end gap-2 sm:col-span-2 lg:col-span-5 lg:justify-end">
|
||||
<a
|
||||
href="/carbets"
|
||||
className="rounded-md border border-zinc-300 px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-50"
|
||||
>
|
||||
Réinitialiser
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-700"
|
||||
>
|
||||
Rechercher
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
87
src/app/carbets/page.tsx
Normal file
87
src/app/carbets/page.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import type { Metadata } from "next";
|
||||
|
||||
import {
|
||||
listPublishedRivers,
|
||||
parseSearchFilters,
|
||||
searchCarbets,
|
||||
type RawSearchParams,
|
||||
} from "@/lib/carbet-search";
|
||||
|
||||
import { CarbetCard } from "./_components/carbet-card";
|
||||
import { SearchFilters } from "./_components/search-filters";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Rechercher un carbet",
|
||||
description:
|
||||
"Explorez les carbets fluviaux de Guyane disponibles sur Karbé : filtrez par fleuve, dates de séjour et capacité d'accueil.",
|
||||
alternates: { canonical: "/carbets" },
|
||||
openGraph: {
|
||||
title: "Rechercher un carbet — Karbé",
|
||||
description:
|
||||
"Trouvez un carbet authentique le long des fleuves de Guyane. Filtrez par fleuve, dates et capacité.",
|
||||
type: "website",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function CarbetsSearchPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<RawSearchParams>;
|
||||
}) {
|
||||
const raw = await searchParams;
|
||||
const filters = parseSearchFilters(raw);
|
||||
|
||||
const [results, rivers] = await Promise.all([
|
||||
searchCarbets(filters),
|
||||
listPublishedRivers(),
|
||||
]);
|
||||
|
||||
const hasActiveFilters = Boolean(
|
||||
filters.river ||
|
||||
filters.startDate ||
|
||||
filters.endDate ||
|
||||
filters.capacity,
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-6xl flex-1 px-6 py-10">
|
||||
<header className="mb-6">
|
||||
<h1 className="text-3xl font-semibold text-zinc-900 sm:text-4xl">
|
||||
Carbets fluviaux de Guyane
|
||||
</h1>
|
||||
<p className="mt-2 max-w-2xl text-base text-zinc-600">
|
||||
Sélectionnez votre fleuve, vos dates et le nombre de voyageurs pour
|
||||
découvrir les carbets disponibles, depuis le Maroni jusqu'à
|
||||
l'Oyapock.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<SearchFilters filters={filters} rivers={rivers} />
|
||||
|
||||
<section className="mt-8" aria-live="polite">
|
||||
<h2 className="sr-only">Résultats de la recherche</h2>
|
||||
{results.length === 0 ? (
|
||||
<p className="rounded-md border border-dashed border-zinc-300 px-6 py-12 text-center text-sm text-zinc-500">
|
||||
{hasActiveFilters
|
||||
? "Aucun carbet ne correspond à votre recherche. Essayez d'élargir les filtres."
|
||||
: "Aucun carbet publié pour le moment. Revenez bientôt !"}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-4 text-sm text-zinc-600">
|
||||
{results.length} carbet{results.length > 1 ? "s" : ""} trouvé
|
||||
{results.length > 1 ? "s" : ""}.
|
||||
</p>
|
||||
<ul className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{results.map((carbet) => (
|
||||
<li key={carbet.id}>
|
||||
<CarbetCard carbet={carbet} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,10 +12,24 @@ const geistMono = Geist_Mono({
|
|||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Karbé — carbets fluviaux de Guyane",
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: {
|
||||
default: "Karbé — carbets fluviaux de Guyane",
|
||||
template: "%s | Karbé",
|
||||
},
|
||||
description:
|
||||
"Karbé, la marketplace de location de carbets fluviaux de Guyane.",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: "Karbé",
|
||||
locale: "fr_FR",
|
||||
title: "Karbé — carbets fluviaux de Guyane",
|
||||
description:
|
||||
"La marketplace pour louer des carbets fluviaux le long des fleuves de Guyane.",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center bg-zinc-50 px-6 dark:bg-black">
|
||||
|
|
@ -10,6 +12,20 @@ export default function Home() {
|
|||
Connecter voyageurs et hôtes pour des séjours authentiques au cœur de
|
||||
la forêt amazonienne.
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||
<Link
|
||||
href="/carbets"
|
||||
className="rounded-md bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-emerald-700"
|
||||
>
|
||||
Découvrir les carbets
|
||||
</Link>
|
||||
<Link
|
||||
href="/espace-hote"
|
||||
className="rounded-md border border-zinc-300 px-5 py-2.5 text-sm font-medium text-zinc-700 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-900"
|
||||
>
|
||||
Espace hôte
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
16
src/app/robots.ts
Normal file
16
src/app/robots.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { MetadataRoute } from "next";
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: ["/admin", "/espace-hote", "/api/", "/connexion"],
|
||||
},
|
||||
],
|
||||
sitemap: `${siteUrl.replace(/\/+$/, "")}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
38
src/app/sitemap.ts
Normal file
38
src/app/sitemap.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import type { MetadataRoute } from "next";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { CarbetStatus } from "@/generated/prisma/enums";
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
|
||||
|
||||
function abs(path: string): string {
|
||||
return `${siteUrl.replace(/\/+$/, "")}${path}`;
|
||||
}
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const staticRoutes: MetadataRoute.Sitemap = [
|
||||
{ url: abs("/"), changeFrequency: "weekly", priority: 1 },
|
||||
{ url: abs("/carbets"), changeFrequency: "daily", priority: 0.9 },
|
||||
{ url: abs("/cgv"), changeFrequency: "yearly", priority: 0.2 },
|
||||
{ url: abs("/mentions-legales"), changeFrequency: "yearly", priority: 0.2 },
|
||||
{
|
||||
url: abs("/politique-de-confidentialite"),
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.2,
|
||||
},
|
||||
];
|
||||
|
||||
const carbets = await prisma.carbet.findMany({
|
||||
where: { status: CarbetStatus.PUBLISHED },
|
||||
select: { slug: true, updatedAt: true },
|
||||
});
|
||||
|
||||
const carbetRoutes: MetadataRoute.Sitemap = carbets.map((carbet) => ({
|
||||
url: abs(`/carbets/${carbet.slug}`),
|
||||
lastModified: carbet.updatedAt,
|
||||
changeFrequency: "weekly",
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
return [...staticRoutes, ...carbetRoutes];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue